diff --git a/.ci/flake8_wrapper.sh b/.ci/flake8_wrapper.sh index e606be9d1d0..63108bc4903 100755 --- a/.ci/flake8_wrapper.sh +++ b/.ci/flake8_wrapper.sh @@ -2,6 +2,11 @@ set -e +# Run broad range of checks over specific white-listed files, this uses +# the default checks defined in setup.cfg. while read p; do flake8 $(eval echo "$p") done <.ci/pep8_sources.txt + +# Look for obviously broken stuff lots more places. +flake8 --select=E901,E902,F821,F822,F823,F831 --exclude lib/galaxy/util/pastescript/serve.py lib/{galaxy,tool_shed} test/{api,unit} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7888a623415..4f1503dad1d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ This document briefly describes how to contribute to the [core galaxy project](https://github.com/galaxyproject/galaxy) - also checkout our 2013 Galaxy Community Conference presentation on the topic -([video](http://vimeo.com/channels/581875/73486255), +([video](https://vimeo.com/channels/581875/73486255), [presentation](https://wiki.galaxyproject.org/Documents/Presentations/GCC2013?action=AttachFile&do=view&target=BakerContribute.pdf)). For information on contributing more broadly to the Galaxy ecosystem and a deeper discussion of some of these points - please see the diff --git a/client/galaxy/scripts/mvc/dataset/dataset-model.js b/client/galaxy/scripts/mvc/dataset/dataset-model.js index b563965cedf..6c38e09f54d 100644 --- a/client/galaxy/scripts/mvc/dataset/dataset-model.js +++ b/client/galaxy/scripts/mvc/dataset/dataset-model.js @@ -57,7 +57,7 @@ var DatasetAssociation = Backbone.Model.extend( BASE_MVC.LoggableMixin ).extend( this._setUpListeners(); }, - + /** returns misc. web urls for rendering things like re-run, display, etc. */ _generateUrls : function(){ //TODO: would be nice if the API did this @@ -251,6 +251,11 @@ var DatasetAssociationCollection = Backbone.Collection.extend( BASE_MVC.Loggable urlRoot : (( window.galaxy_config && galaxy_config.root )?( galaxy_config.root ):( '/' )) + 'api/datasets', + /** url fn */ + url : function(){ + return this.urlRoot + }, + // ........................................................................ common queries /** Get the ids of every item in this collection * @returns array of encoded ids diff --git a/client/galaxy/scripts/mvc/form/form-data.js b/client/galaxy/scripts/mvc/form/form-data.js index 5e1f23ce1fb..0562d97fbf9 100644 --- a/client/galaxy/scripts/mvc/form/form-data.js +++ b/client/galaxy/scripts/mvc/form/form-data.js @@ -96,9 +96,9 @@ return Backbone.Model.extend({ convert(job_input_id, head[input.id + '-section-' + selectedCase]); } break; - // handle custom sub sections + // handle sections case 'section': - convert('', node); + convert(job_input_id, node); break; default: // get field @@ -191,6 +191,9 @@ return Backbone.Model.extend({ search (index, node.cases[selectedCase].inputs); } break; + case 'section': + search (index, node.inputs); + break; default: var input_id = self.map_dict[index]; if (input_id) { diff --git a/client/galaxy/scripts/mvc/form/form-section.js b/client/galaxy/scripts/mvc/form/form-section.js index f2f4a22785c..3c1c2092451 100644 --- a/client/galaxy/scripts/mvc/form/form-section.js +++ b/client/galaxy/scripts/mvc/form/form-section.js @@ -286,7 +286,7 @@ define(['utils/utils', }); // show sub section if requested - if (input_def.expand) { + if (input_def.expanded) { portlet.$header.trigger('click'); } diff --git a/client/galaxy/scripts/mvc/form/form-view.js b/client/galaxy/scripts/mvc/form/form-view.js index 2eb5d9d2678..312d9e8c2b7 100644 --- a/client/galaxy/scripts/mvc/form/form-view.js +++ b/client/galaxy/scripts/mvc/form/form-view.js @@ -200,6 +200,13 @@ define(['utils/utils', 'mvc/ui/ui-portlet', 'mvc/ui/ui-misc', inputs : this.options.inputs }); + // switch to classic tool form mako if the form definition is incompatible + if (this.incompatible) { + this.$el.hide(); + $('#tool-form-classic').show(); + return; + } + // create portlet this.portlet = new Portlet.View({ icon : 'fa-wrench', diff --git a/client/galaxy/scripts/mvc/history/history-panel-edit.js b/client/galaxy/scripts/mvc/history/history-panel-edit.js index ae1de05ef56..7f68f324287 100644 --- a/client/galaxy/scripts/mvc/history/history-panel-edit.js +++ b/client/galaxy/scripts/mvc/history/history-panel-edit.js @@ -360,7 +360,7 @@ var HistoryPanelEdit = _super.extend( * @param {Model} the item model to check */ _handleHdaVisibleChange : function( itemModel ){ - if( itemModel.hidden() && !this.storage.showHidden ){ + if( itemModel.hidden() && !this.showHidden ){ this.removeItemView( itemModel ); } this._renderCounts(); diff --git a/client/galaxy/scripts/mvc/history/multi-panel.js b/client/galaxy/scripts/mvc/history/multi-panel.js index 0175267f6fa..cb030684db2 100644 --- a/client/galaxy/scripts/mvc/history/multi-panel.js +++ b/client/galaxy/scripts/mvc/history/multi-panel.js @@ -968,7 +968,9 @@ var MultiPanelColumns = Backbone.View.extend( baseMVC.LoggableMixin ).extend({ // when scrolling - check for histories now in view: they will fire 'in-view' and queueHdaLoading if necc. //TODO:?? might be able to simplify and not use pub-sub - var debouncedInView = _.debounce( _.bind( this.checkColumnsInView, this ), 100 ); + var debouncedInView = _.debounce( function _debouncedInner(){ + multipanel.checkColumnsInView(); + }, 100 ); this.$( '.middle' ).parent().scroll( debouncedInView ); }, diff --git a/config/datatypes_conf.xml.sample b/config/datatypes_conf.xml.sample index 726f231384c..42b874b46cc 100644 --- a/config/datatypes_conf.xml.sample +++ b/config/datatypes_conf.xml.sample @@ -186,6 +186,7 @@ + @@ -285,6 +286,7 @@ + diff --git a/config/galaxy.ini.sample b/config/galaxy.ini.sample index 3ddf929677b..d0b176c842e 100644 --- a/config/galaxy.ini.sample +++ b/config/galaxy.ini.sample @@ -218,7 +218,7 @@ paste.app_factory = galaxy.web.buildapp:app_factory #tool_data_path = tool-data # Directory where Tool Data Table related files will be placed -# when installed from a ToolShed. Defaults to tool_data_path +# when installed from a ToolShed. Defaults to tool_data_path. #shed_tool_data_path = tool-data # File containing old-style genome builds @@ -369,7 +369,7 @@ paste.app_factory = galaxy.web.buildapp:app_factory # URL of the support resource for the galaxy instance. Used in activation # emails. -#instance_resource_url = http://wiki.galaxyproject.org/ +#instance_resource_url = https://wiki.galaxyproject.org/ # E-mail domains blacklist is used for filtering out users that are using # disposable email address during the registration. If their address domain @@ -465,7 +465,7 @@ nglims_config_file = tool-data/nglims.yaml # Format string used when showing date and time information. # The string may contain: # - the directives used by Python time.strftime() function (see -# http://docs.python.org/2/library/time.html#time.strftime ), +# https://docs.python.org/2/library/time.html#time.strftime ), # - $locale (complete format string for the server locale), # - $iso8601 (complete format string as specified by ISO 8601 international # standard). @@ -476,6 +476,15 @@ nglims_config_file = tool-data/nglims.yaml # IPython Docker container for communicating back with Galaxy via the API. #galaxy_infrastructure_url = http://localhost:8080 +# If the above URL cannot be determined ahead of time in dynamic environments +# but the port which should be used to access Galaxy can be - this should be +# set to prevent Galaxy from having to guess. For example if Galaxy is sitting +# behind a proxy with REMOTE_USER enabled - infrastructure shouldn't talk to +# Python processes directly and this should be set to 80 or 443, etc... If +# unset this file will be read for a server block defining a port corresponding +# to the webapp. +#galaxy_infrastructure_web_port = 8080 + # The URL of the page to display in Galaxy's middle pane when loaded. This can # be an absolute or relative URL. #welcome_url = /static/welcome.html @@ -484,22 +493,22 @@ nglims_config_file = tool-data/nglims.yaml #logo_url = / # The URL linked by the "Wiki" link in the "Help" menu. -#wiki_url = http://wiki.galaxyproject.org/ +#wiki_url = https://wiki.galaxyproject.org/ # The URL linked by the "Support" link in the "Help" menu. -#support_url = http://wiki.galaxyproject.org/Support +#support_url = https://wiki.galaxyproject.org/Support # The URL linked by the "How to Cite Galaxy" link in the "Help" menu. -#citation_url = http://wiki.galaxyproject.org/CitingGalaxy +#citation_url = https://wiki.galaxyproject.org/CitingGalaxy #The URL linked by the "Search" link in the "Help" menu. #search_url = http://galaxyproject.org/search/usegalaxy/ #The URL linked by the "Mailing Lists" link in the "Help" menu. -#mailing_lists_url = http://wiki.galaxyproject.org/MailingLists +#mailing_lists_url = https://wiki.galaxyproject.org/MailingLists #The URL linked by the "Videos" link in the "Help" menu. -#videos_url = http://vimeo.com/galaxyproject +#screencasts_url = https://vimeo.com/galaxyproject # The URL linked by the "Terms and Conditions" link in the "Help" menu, as well # as on the user registration and login forms and in the activation emails. @@ -576,6 +585,13 @@ nglims_config_file = tool-data/nglims.yaml # requests. #nginx_upload_path = False +# Galaxy can also use nginx_upload_module to receive files staged out upon job +# completion by remote job runners (i.e. Pulsar) that initiate staging +# operations on the remote end. See the Galaxy nginx documentation for the +# corresponding nginx configuration. +#nginx_upload_job_files_store = False +#nginx_upload_job_files_path = False + # Have Galaxy manage dynamic proxy component for routing requests to other # services based on Galaxy's session cookie. It will attempt to do this by # default though you do need to install node+npm and do an npm install from @@ -603,7 +619,7 @@ nglims_config_file = tool-data/nglims.yaml # -- Logging and Debugging # Verbosity of console log messages. Acceptable values can be found here: -# http://docs.python.org/library/logging.html#logging-levels +# https://docs.python.org/2/library/logging.html#logging-levels #log_level = DEBUG # Print database operations to the server log (warning, quite verbose!). @@ -716,16 +732,16 @@ use_interactive = True # -- 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 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. +# 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 @@ -805,11 +821,12 @@ use_interactive = True # public. #new_user_dataset_access_role_default_private = False -# Expose user list. Setting this to true will expose the user list to authenticated users. This -# makes sharing datasets in smaller galaxy instances much easier as they can type a name/email and -# have the correct user show up. This makes less sense on large public galaxy instances where -# that data shouldn't be exposed. For semi-public galaxies, it may make sense to expose just the -# username and not email, or vice versa. +# Expose user list. Setting this to True will expose the user list to +# authenticated users. This makes sharing datasets in smaller galaxy instances +# much easier as they can type a name/email and have the correct user show up. +# This makes less sense on large public Galaxy instances where that data +# shouldn't be exposed. For semi-public Galaxies, it may make sense to expose +# just the username and not email, or vice versa. #expose_user_name = False #expose_user_email = False @@ -856,8 +873,8 @@ use_interactive = True #openid_consumer_cache_path = database/openid_consumer_cache # XML config file that allows the use of different authentication providers -# (e.g. Active Directory) instead or in addition to local authentication -# (.sample is used if default does not exist). +# (e.g. LDAP) instead or in addition to local authentication (.sample is used +# if default does not exist). #auth_config_file = config/auth_conf.xml # Optional list of email addresses of API users who can make calls on behalf of @@ -906,14 +923,14 @@ use_interactive = True #expose_dataset_path = False # Data manager configuration options -# Allow non-admin users to view available Data Manager options +# Allow non-admin users to view available Data Manager options. #enable_data_manager_user_view = False # File where Data Managers are configured (.sample used if default does not -# exist) +# exist). #data_manager_config_file = config/data_manager_conf.xml -# File where Tool Shed based Data Managers are configured +# File where Tool Shed based Data Managers are configured. #shed_data_manager_config_file = config/shed_data_manager_conf.xml -# Directory to store Data Manager based tool-data; defaults to tool_data_path +# Directory to store Data Manager based tool-data; defaults to tool_data_path. #galaxy_data_manager_data_path = tool-data # -- Job Execution @@ -976,6 +993,17 @@ use_interactive = True # always, onsuccess, never #cleanup_job = always +# For sites where all users in Galaxy match users on the system on which Galaxy +# runs, the DRMAA job runner can be configured to submit jobs to the DRM as the +# actual user instead of as the user running the Galaxy server process. For +# details on these options, see the documentation at: +# +# https://wiki.galaxyproject.org/Admin/Config/Performance/Cluster +# +#drmaa_external_runjob_script = scripts/drmaa_external_runner.py +#drmaa_external_killjob_script = scripts/drmaa_external_killer.py +#external_chown_script = scripts/external_chown_script.py + # File to source to set up the environment when running jobs. By default, the # environment in which the Galaxy server starts is used when running jobs # locally, and the environment set up per the DRM's submission method and @@ -987,7 +1015,6 @@ use_interactive = True # individually. #environment_setup_file = None - # Optional file containing job resource data entry fields definition. # These fields will be presented to users in the tool forms and allow them to # overwrite default job resources such as number of processors, memory and diff --git a/config/job_conf.xml.sample_advanced b/config/job_conf.xml.sample_advanced index 2290c5f1ad0..3b43e48fed4 100644 --- a/config/job_conf.xml.sample_advanced +++ b/config/job_conf.xml.sample_advanced @@ -252,10 +252,7 @@ --> - - scripts/drmaa_external_runner.py - scripts/drmaa_external_killer.py - scripts/external_chown_script.py + diff --git a/config/plugins/visualizations/scatterplot/src/scatterplot-display.js b/config/plugins/visualizations/scatterplot/src/scatterplot-display.js index 7f36486e09f..0429f1472bb 100644 --- a/config/plugins/visualizations/scatterplot/src/scatterplot-display.js +++ b/config/plugins/visualizations/scatterplot/src/scatterplot-display.js @@ -20,7 +20,7 @@ var ScatterplotDisplay = Backbone.View.extend({ var view = this, config = this.model.get( 'config' ), //TODO: very tied to datasets - should be generalized eventually - xhr = jQuery.getJSON( '/api/datasets/' + this.dataset.id, { + xhr = jQuery.getJSON(((typeof parent.galaxy_config.root == 'undefined') ? '/' : parent.galaxy_config.root) + 'api/datasets/' + this.dataset.id, { data_type : 'raw_data', provider : 'dataset-column', limit : config.pagination.perPage, diff --git a/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js b/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js index fc44b025410..5ef5bdc44fc 100644 --- a/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js +++ b/config/plugins/visualizations/scatterplot/static/scatterplot-edit.js @@ -1 +1 @@ -function scatterplot(a,b,c){function d(){var a={v:{},h:{}};return a.v.lines=p.selectAll("line.v-grid-line").data(m.x.ticks(q.x.fn.ticks()[0])),a.v.lines.enter().append("svg:line").classed("grid-line v-grid-line",!0),a.v.lines.attr("x1",m.x).attr("x2",m.x).attr("y1",0).attr("y2",b.height),a.v.lines.exit().remove(),a.h.lines=p.selectAll("line.h-grid-line").data(m.y.ticks(q.y.fn.ticks()[0])),a.h.lines.enter().append("svg:line").classed("grid-line h-grid-line",!0),a.h.lines.attr("x1",0).attr("x2",b.width).attr("y1",m.y).attr("y2",m.y),a.h.lines.exit().remove(),a}function e(){return t.attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).style("display","block").filter(function(){var a=d3.select(this).attr("cx"),c=d3.select(this).attr("cy");return 0>a||a>b.width?!0:0>c||c>b.height?!0:!1}).style("display","none")}function f(){$(".chart-info-box").remove(),q.redraw(),e(),s=d(),$(o.node()).trigger("zoom.scatterplot",{scale:n.scale(),translate:n.translate()})}function g(a,c,d){return c+=8,$(['
',void 0!==b.idColumn?"
"+d[b.idColumn]+"
":"","
",j(d),"
","
",k(d),"
","
"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a){return a[b.xColumn]},k=function(a){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,30]).scale(b.scale||1).translate(b.translate||[0,0]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.xTicks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.yTicks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=6;q.x.label=o.append("text").attr("id","x-axis-label").attr("class","axis-label").text(b.xLabel).attr("text-anchor","middle").attr("dominant-baseline","text-after-edge").attr("x",b.width/2+b.margin.left).attr("y",b.height+b.margin.bottom+b.margin.top-r),q.y.label=o.append("text").attr("id","y-axis-label").attr("class","axis-label").text(b.yLabel).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",0);t.transition().duration(b.animDuration).attr("r",b.datapointSize),e(),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.classed("highlight",!0).style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")= 2.0.0-beta.1"],main:function(a,b,c,d){var e,f="function",g=b.helperMissing,h=this.escapeExpression;return'

\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n

\n\n
\n \n
'+h((e=null!=(e=b.datapointSize||(null!=a?a.datapointSize:a))?e:g,typeof e===f?e.call(a,{name:"datapointSize",hash:{},data:d}):e))+'
\n
\n

\n Size of the graphic representation of each data point\n

\n
\n\n
\n \n
'+h((e=null!=(e=b.width||(null!=a?a.width:a))?e:g,typeof e===f?e.call(a,{name:"width",hash:{},data:d}):e))+'
\n
\n

\n (not including chart margins and axes)\n

\n
\n\n
\n \n
'+h((e=null!=(e=b.height||(null!=a?a.height:a))?e:g,typeof e===f?e.call(a,{name:"height",hash:{},data:d}):e))+'
\n
\n

\n (not including chart margins and axes)\n

\n
\n\n
\n \n \n

\n
\n\n
\n \n \n

\n
\n\n\n'},useData:!0}),this.scatterplot.datacontrol=Handlebars.template({compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){var e,f,g="function",h=b.helperMissing,i='

\n Use the following control to change which columns are used by the chart. Click any cell\n from the last three rows of the table to select the column for the appropriate data.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n

\n\n
    \n
  • X Column: which column values will be used for the x axis of the chart.
  • \n
  • Y Column: which column values will be used for the y axis of the chart.
  • \n
  • ID Column: an additional column value displayed when the user hovers over a data point.\n It may be useful to select unique or categorical identifiers here (such as gene ids).\n
  • \n
\n\n
\n
';return f=null!=(f=b.peek||(null!=a?a.peek:a))?f:h,e=typeof f===g?f.call(a,{name:"peek",hash:{},data:d}):f,null!=e&&(i+=e),i+'
\n
\n\n

\n Note: If it can be determined from the dataset\'s filetype that a column is not numeric,\n that column choice may be disabled for either the x or y axis.\n

\n\n\n'},useData:!0}),this.scatterplot.editor=Handlebars.template({compiler:[6,">= 2.0.0-beta.1"],main:function(){return'
\n \n\n
\n
\n
\n \n
\n
\n\n
\n\n
\n
\n'},useData:!0});var ScatterplotConfigEditor=Backbone.View.extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,this.display=new ScatterplotDisplay({dataset:a.dataset,model:this.model})},render:function(){this.$el.empty().append(ScatterplotConfigEditor.templates.mainLayout({})),this.model.id&&(this.$el.find(".copy-btn").show(),this.$el.find(".save-btn").text("Update saved")),this.$el.find("[title]").tooltip(),this._render_dataControl(),this._render_chartControls(),this._render_chartDisplay();var a=this.model.get("config");return this.model.id&&_.isFinite(a.xColumn)&&_.isFinite(a.yColumn)&&this.renderChart(),this},_getColumnIndecesByType:function(){var a={numeric:[],text:[],all:[]};return _.each(this.dataset.metadata_column_types||[],function(b,c){"int"===b||"float"===b?a.numeric.push(c):("str"===b||"list"===b)&&a.text.push(c),a.all.push(c)}),a.numeric.length<2&&(a.numeric=[]),a},_render_dataControl:function(a){a=a||this.$el;var b=this,c=this.model.get("config"),d=this._getColumnIndecesByType(),e=a.find(".tab-pane#data-control");return e.html(ScatterplotConfigEditor.templates.dataControl({peek:this.dataset.peek})),e.find(".peek").peekColumnSelector({controls:[{label:"X Column",id:"xColumn",selected:c.xColumn,disabled:d.text},{label:"Y Column",id:"yColumn",selected:c.yColumn,disabled:d.text},{label:"ID Column",id:"idColumn",selected:c.idColumn}]}).on("peek-column-selector.change",function(a,c){b.model.set("config",c)}).on("peek-column-selector.rename",function(){}),e.find("[title]").tooltip(),e},_render_chartControls:function(a){function b(){var a=$(this),b=a.slider("value");c.model.set("config",_.object([[a.parent().data("config-key"),b]])),a.siblings(".slider-output").text(b)}a=a||this.$el;var c=this,d=this.model.get("config"),e=a.find("#chart-control");e.html(ScatterplotConfigEditor.templates.chartControl(d));var f={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};e.find(".numeric-slider-input").each(function(){var a=$(this),c=a.attr("data-config-key"),e=_.extend(f[c],{value:d[c],change:b,slide:b});a.find(".slider").slider(e),a.children(".slider-output").text(d[c])});var g=this.dataset.metadata_column_names||[],h=d.xLabel||g[d.xColumn]||"X",i=d.yLabel||g[d.yColumn]||"Y";return e.find('input[name="X-axis-label"]').val(h).on("change",function(){c.model.set("config",{xLabel:$(this).val()})}),e.find('input[name="Y-axis-label"]').val(i).on("change",function(){c.model.set("config",{yLabel:$(this).val()})}),e.find("[title]").tooltip(),e},_render_chartDisplay:function(a){a=a||this.$el;var b=a.find(".tab-pane#chart-display");return this.display.setElement(b),this.display.render(),b.find("[title]").tooltip(),b},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart","click .save-btn":"saveVisualization"},saveVisualization:function(){var a=this;this.model.save().fail(function(b,c,d){console.error(b,c,d),a.trigger("save:error",view),alert("Error loading data:\n"+b.responseText)}).then(function(){a.display.render()})},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.display.fetchData()},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotDisplay=Backbone.View.extend({initialize:function(a){this.data=null,this.dataset=a.dataset,this.lineCount=this.dataset.metadata_data_lines||null},fetchData:function(){this.showLoadingIndicator();var a=this,b=this.model.get("config"),c=jQuery.getJSON("/api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:b.pagination.perPage,offset:b.pagination.currPage*b.pagination.perPage});return c.done(function(b){a.data=b.data,a.trigger("data:fetched",a),a.renderData()}),c.fail(function(b,c,d){console.error(b,c,d),a.trigger("data:error",a),alert("Error loading data:\n"+b.responseText)}),c},showLoadingIndicator:function(){this.$el.find(".scatterplot-data-info").html(['
','','loading...',"
"].join(""))},template:function(){var a=['
','
','

','','',"
",'
','
',"
","
","",'
'].join("");return a},render:function(){return this.$el.addClass("scatterplot-display").html(this.template()),this.data&&this.renderData(),this},renderData:function(){this.renderLeftControls(),this.renderRightControls(),this.renderPlot(this.data),this.getStats()},renderLeftControls:function(){var a=this,b=this.model.get("config");return this.$el.find(".controls .left .page-control").pagination({startingPage:b.pagination.currPage,perPage:b.pagination.perPage,totalDataSize:this.lineCount,currDataSize:this.data.length}).off().on("pagination.page-change",function(c,d){b.pagination.currPage=d,a.model.set("config",{pagination:b.pagination}),a.resetZoom(),a.fetchData()}),this},renderRightControls:function(){var a=this;this.setLineInfo(this.data),this.$el.find(".stats-toggle-btn").off().click(function(){a.toggleStats()}),this.$el.find(".rerender-btn").off().click(function(){a.resetZoom(),a.renderPlot(this.data)})},renderPlot:function(){var a=this,b=this.$el.find("svg");this.toggleStats(!1),b.off().empty().show().on("zoom.scatterplot",function(b,c){a.model.set("config",c)}),scatterplot(b.get(0),this.model.get("config"),this.data)},setLineInfo:function(a,b){if(a){var c=this.model.get("config"),d=this.lineCount||"an unknown total",e=c.pagination.currPage*c.pagination.perPage,f=e+a.length;this.$el.find(".controls p.scatterplot-data-info").text([e+1,"to",f,"of",d].join(" "))}else this.$el.find(".controls p.scatterplot-data-info").html(b||"");return this},resetZoom:function(a,b){return a=void 0!==a?a:1,b=void 0!==b?b:[0,0],this.model.set("config",{scale:a,translate:b}),this},getStats:function(){if(this.data){var a=this,b=this.model.get("config"),c=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js");c.postMessage({data:this.data,keys:[b.xColumn,b.yColumn]}),c.onerror=function(){c.terminate()},c.onmessage=function(b){a.renderStats(b.data)}}},renderStats:function(a){var b=this.model.get("config"),c=this.$el.find(".stats-display"),d=b.xLabel,e=b.yLabel,f=$("").addClass("table").append([""].join("")).append(_.map(a,function(a,b){return $([""].join(""))}));c.empty().append(f)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},xTicks:10,xLabel:"X",yTicks:10,yLabel:"Y",datapointSize:4,animDuration:500,scale:1,translate:[0,0]}}}); \ No newline at end of file +function scatterplot(a,b,c){function d(){var a={v:{},h:{}};return a.v.lines=p.selectAll("line.v-grid-line").data(m.x.ticks(q.x.fn.ticks()[0])),a.v.lines.enter().append("svg:line").classed("grid-line v-grid-line",!0),a.v.lines.attr("x1",m.x).attr("x2",m.x).attr("y1",0).attr("y2",b.height),a.v.lines.exit().remove(),a.h.lines=p.selectAll("line.h-grid-line").data(m.y.ticks(q.y.fn.ticks()[0])),a.h.lines.enter().append("svg:line").classed("grid-line h-grid-line",!0),a.h.lines.attr("x1",0).attr("x2",b.width).attr("y1",m.y).attr("y2",m.y),a.h.lines.exit().remove(),a}function e(){return t.attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).style("display","block").filter(function(a,c){var d=d3.select(this).attr("cx"),e=d3.select(this).attr("cy");return 0>d||d>b.width?!0:0>e||e>b.height?!0:!1}).style("display","none")}function f(a,b,c){$(".chart-info-box").remove(),q.redraw(),e(),s=d(),$(o.node()).trigger("zoom.scatterplot",{scale:n.scale(),translate:n.translate()})}function g(a,c,d){return c+=8,$(['
',void 0!==b.idColumn?"
"+d[b.idColumn]+"
":"","
",j(d),"
","
",k(d),"
","
"].join("")).css({top:a,left:c,"z-index":2})}var h=function(a,b){return"translate("+a+","+b+")"},i=function(a,b,c){return"rotate("+a+","+b+","+c+")"},j=function(a,c){return a[b.xColumn]},k=function(a,c){return a[b.yColumn]},l={x:{extent:d3.extent(c,j)},y:{extent:d3.extent(c,k)}},m={x:d3.scale.linear().domain(l.x.extent).range([0,b.width]),y:d3.scale.linear().domain(l.y.extent).range([b.height,0])},n=d3.behavior.zoom().x(m.x).y(m.y).scaleExtent([1,30]).scale(b.scale||1).translate(b.translate||[0,0]),o=d3.select(a).attr("class","scatterplot").attr("width","100%").attr("height",b.height+(b.margin.top+b.margin.bottom)),p=o.append("g").attr("class","content").attr("transform",h(b.margin.left,b.margin.top)).call(n);p.append("rect").attr("class","zoom-rect").attr("width",b.width).attr("height",b.height).style("fill","transparent");var q={x:{},y:{}};q.x.fn=d3.svg.axis().orient("bottom").scale(m.x).ticks(b.xTicks).tickFormat(d3.format("s")),q.y.fn=d3.svg.axis().orient("left").scale(m.y).ticks(b.yTicks).tickFormat(d3.format("s")),q.x.g=p.append("g").attr("class","x axis").attr("transform",h(0,b.height)).call(q.x.fn),q.y.g=p.append("g").attr("class","y axis").call(q.y.fn);var r=6;q.x.label=o.append("text").attr("id","x-axis-label").attr("class","axis-label").text(b.xLabel).attr("text-anchor","middle").attr("dominant-baseline","text-after-edge").attr("x",b.width/2+b.margin.left).attr("y",b.height+b.margin.bottom+b.margin.top-r),q.y.label=o.append("text").attr("id","y-axis-label").attr("class","axis-label").text(b.yLabel).attr("text-anchor","middle").attr("dominant-baseline","text-before-edge").attr("x",r).attr("y",b.height/2).attr("transform",i(-90,r,b.height/2)),q.redraw=function(){o.select(".x.axis").call(q.x.fn),o.select(".y.axis").call(q.y.fn)};var s=d(),t=p.selectAll(".glyph").data(c).enter().append("svg:circle").classed("glyph",!0).attr("cx",function(a,b){return m.x(j(a,b))}).attr("cy",function(a,b){return m.y(k(a,b))}).attr("r",0);t.transition().duration(b.animDuration).attr("r",b.datapointSize),e(),n.on("zoom",f),t.on("mouseover",function(a,c){var d=d3.select(this);d.classed("highlight",!0).style("fill","red").style("fill-opacity",1),p.append("line").attr("stroke","red").attr("stroke-width",1).attr("x1",d.attr("cx")-b.datapointSize).attr("y1",d.attr("cy")).attr("x2",0).attr("y2",d.attr("cy")).classed("hoverline",!0),d.attr("cy")= 2.0.0-beta.1"],main:function(a,b,c,d){var e,f="function",g=b.helperMissing,h=this.escapeExpression;return'

\n Use the following controls to how the chart is displayed.\n The slide controls can be moved by the mouse or, if the \'handle\' is in focus, your keyboard\'s arrow keys.\n Move the focus between controls by using the tab or shift+tab keys on your keyboard.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n

\n\n
\n \n
'+h((e=null!=(e=b.datapointSize||(null!=a?a.datapointSize:a))?e:g,typeof e===f?e.call(a,{name:"datapointSize",hash:{},data:d}):e))+'
\n
\n

\n Size of the graphic representation of each data point\n

\n
\n\n
\n \n
'+h((e=null!=(e=b.width||(null!=a?a.width:a))?e:g,typeof e===f?e.call(a,{name:"width",hash:{},data:d}):e))+'
\n
\n

\n (not including chart margins and axes)\n

\n
\n\n
\n \n
'+h((e=null!=(e=b.height||(null!=a?a.height:a))?e:g,typeof e===f?e.call(a,{name:"height",hash:{},data:d}):e))+'
\n
\n

\n (not including chart margins and axes)\n

\n
\n\n
\n \n \n

\n
\n\n
\n \n \n

\n
\n\n\n'},useData:!0}),this.scatterplot.datacontrol=Handlebars.template({compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){var e,f,g="function",h=b.helperMissing,i='

\n Use the following control to change which columns are used by the chart. Click any cell\n from the last three rows of the table to select the column for the appropriate data.\n Use the \'Draw\' button to render (or re-render) the chart with the current settings.\n

\n\n
    \n
  • X Column: which column values will be used for the x axis of the chart.
  • \n
  • Y Column: which column values will be used for the y axis of the chart.
  • \n
  • ID Column: an additional column value displayed when the user hovers over a data point.\n It may be useful to select unique or categorical identifiers here (such as gene ids).\n
  • \n
\n\n
\n
';return f=null!=(f=b.peek||(null!=a?a.peek:a))?f:h,e=typeof f===g?f.call(a,{name:"peek",hash:{},data:d}):f,null!=e&&(i+=e),i+'
\n
\n\n

\n Note: If it can be determined from the dataset\'s filetype that a column is not numeric,\n that column choice may be disabled for either the x or y axis.\n

\n\n\n'},useData:!0}),this.scatterplot.editor=Handlebars.template({compiler:[6,">= 2.0.0-beta.1"],main:function(a,b,c,d){return'
\n \n\n
\n
\n
\n \n
\n
\n\n
\n\n
\n
\n'},useData:!0});var ScatterplotConfigEditor=Backbone.View.extend({className:"scatterplot-control-form",initialize:function(a){if(this.model||(this.model=new Visualization({type:"scatterplot"})),!a||!a.dataset)throw new Error("ScatterplotConfigEditor requires a dataset");this.dataset=a.dataset,this.display=new ScatterplotDisplay({dataset:a.dataset,model:this.model})},render:function(){this.$el.empty().append(ScatterplotConfigEditor.templates.mainLayout({})),this.model.id&&(this.$el.find(".copy-btn").show(),this.$el.find(".save-btn").text("Update saved")),this.$el.find("[title]").tooltip(),this._render_dataControl(),this._render_chartControls(),this._render_chartDisplay();var a=this.model.get("config");return this.model.id&&_.isFinite(a.xColumn)&&_.isFinite(a.yColumn)&&this.renderChart(),this},_getColumnIndecesByType:function(){var a={numeric:[],text:[],all:[]};return _.each(this.dataset.metadata_column_types||[],function(b,c){"int"===b||"float"===b?a.numeric.push(c):("str"===b||"list"===b)&&a.text.push(c),a.all.push(c)}),a.numeric.length<2&&(a.numeric=[]),a},_render_dataControl:function(a){a=a||this.$el;var b=this,c=this.model.get("config"),d=this._getColumnIndecesByType(),e=a.find(".tab-pane#data-control");return e.html(ScatterplotConfigEditor.templates.dataControl({peek:this.dataset.peek})),e.find(".peek").peekColumnSelector({controls:[{label:"X Column",id:"xColumn",selected:c.xColumn,disabled:d.text},{label:"Y Column",id:"yColumn",selected:c.yColumn,disabled:d.text},{label:"ID Column",id:"idColumn",selected:c.idColumn}]}).on("peek-column-selector.change",function(a,c){b.model.set("config",c)}).on("peek-column-selector.rename",function(a,b){}),e.find("[title]").tooltip(),e},_render_chartControls:function(a){function b(){var a=$(this),b=a.slider("value");c.model.set("config",_.object([[a.parent().data("config-key"),b]])),a.siblings(".slider-output").text(b)}a=a||this.$el;var c=this,d=this.model.get("config"),e=a.find("#chart-control");e.html(ScatterplotConfigEditor.templates.chartControl(d));var f={datapointSize:{min:2,max:10,step:1},width:{min:200,max:800,step:20},height:{min:200,max:800,step:20}};e.find(".numeric-slider-input").each(function(){var a=$(this),c=a.attr("data-config-key"),e=_.extend(f[c],{value:d[c],change:b,slide:b});a.find(".slider").slider(e),a.children(".slider-output").text(d[c])});var g=this.dataset.metadata_column_names||[],h=d.xLabel||g[d.xColumn]||"X",i=d.yLabel||g[d.yColumn]||"Y";return e.find('input[name="X-axis-label"]').val(h).on("change",function(){c.model.set("config",{xLabel:$(this).val()})}),e.find('input[name="Y-axis-label"]').val(i).on("change",function(){c.model.set("config",{yLabel:$(this).val()})}),e.find("[title]").tooltip(),e},_render_chartDisplay:function(a){a=a||this.$el;var b=a.find(".tab-pane#chart-display");return this.display.setElement(b),this.display.render(),b.find("[title]").tooltip(),b},events:{"change #include-id-checkbox":"toggleThirdColumnSelector","click #data-control .render-button":"renderChart","click #chart-control .render-button":"renderChart","click .save-btn":"saveVisualization"},saveVisualization:function(){var a=this;this.model.save().fail(function(b,c,d){console.error(b,c,d),a.trigger("save:error",view),alert("Error loading data:\n"+b.responseText)}).then(function(){a.display.render()})},toggleThirdColumnSelector:function(){this.$el.find('select[name="idColumn"]').parent().toggle()},renderChart:function(){this.$el.find(".nav li.disabled").removeClass("disabled"),this.$el.find("ul.nav").find('a[href="#chart-display"]').tab("show"),this.display.fetchData()},toString:function(){return"ScatterplotConfigEditor("+(this.dataset?this.dataset.id:"")+")"}});ScatterplotConfigEditor.templates={mainLayout:scatterplot.editor,dataControl:scatterplot.datacontrol,chartControl:scatterplot.chartcontrol};var ScatterplotDisplay=Backbone.View.extend({initialize:function(a){this.data=null,this.dataset=a.dataset,this.lineCount=this.dataset.metadata_data_lines||null},fetchData:function(){this.showLoadingIndicator();var a=this,b=this.model.get("config"),c=jQuery.getJSON(("undefined"==typeof parent.galaxy_config.root?"/":parent.galaxy_config.root)+"api/datasets/"+this.dataset.id,{data_type:"raw_data",provider:"dataset-column",limit:b.pagination.perPage,offset:b.pagination.currPage*b.pagination.perPage});return c.done(function(b){a.data=b.data,a.trigger("data:fetched",a),a.renderData()}),c.fail(function(b,c,d){console.error(b,c,d),a.trigger("data:error",a),alert("Error loading data:\n"+b.responseText)}),c},showLoadingIndicator:function(){this.$el.find(".scatterplot-data-info").html(['
','','loading...',"
"].join(""))},template:function(){var a=['
','
','

','','',"
",'
','
',"
","
","",'
'].join("");return a},render:function(){return this.$el.addClass("scatterplot-display").html(this.template()),this.data&&this.renderData(),this},renderData:function(){this.renderLeftControls(),this.renderRightControls(),this.renderPlot(this.data),this.getStats()},renderLeftControls:function(){var a=this,b=this.model.get("config");return this.$el.find(".controls .left .page-control").pagination({startingPage:b.pagination.currPage,perPage:b.pagination.perPage,totalDataSize:this.lineCount,currDataSize:this.data.length}).off().on("pagination.page-change",function(c,d){b.pagination.currPage=d,a.model.set("config",{pagination:b.pagination}),a.resetZoom(),a.fetchData()}),this},renderRightControls:function(){var a=this;this.setLineInfo(this.data),this.$el.find(".stats-toggle-btn").off().click(function(){a.toggleStats()}),this.$el.find(".rerender-btn").off().click(function(){a.resetZoom(),a.renderPlot(this.data)})},renderPlot:function(){var a=this,b=this.$el.find("svg");this.toggleStats(!1),b.off().empty().show().on("zoom.scatterplot",function(b,c){a.model.set("config",c)}),scatterplot(b.get(0),this.model.get("config"),this.data)},setLineInfo:function(a,b){if(a){var c=this.model.get("config"),d=this.lineCount||"an unknown total",e=c.pagination.currPage*c.pagination.perPage,f=e+a.length;this.$el.find(".controls p.scatterplot-data-info").text([e+1,"to",f,"of",d].join(" "))}else this.$el.find(".controls p.scatterplot-data-info").html(b||"");return this},resetZoom:function(a,b){return a=void 0!==a?a:1,b=void 0!==b?b:[0,0],this.model.set("config",{scale:a,translate:b}),this},getStats:function(){if(this.data){var a=this,b=this.model.get("config"),c=new Worker("/plugins/visualizations/scatterplot/static/worker-stats.js");c.postMessage({data:this.data,keys:[b.xColumn,b.yColumn]}),c.onerror=function(a){c.terminate()},c.onmessage=function(b){a.renderStats(b.data)}}},renderStats:function(a,b){var c=this.model.get("config"),d=this.$el.find(".stats-display"),e=c.xLabel,f=c.yLabel,g=$("
",d,"",e,"
",b,"",a[0],"",a[1],"
").addClass("table").append([""].join("")).append(_.map(a,function(a,b){return $([""].join(""))}));d.empty().append(g)},toggleStats:function(a){var b=this.$el.find(".stats-display");a=void 0===a?b.is(":hidden"):a,a?(this.$el.find("svg").hide(),b.show(),this.$el.find(".controls .stats-toggle-btn").text("Plot")):(b.hide(),this.$el.find("svg").show(),this.$el.find(".controls .stats-toggle-btn").text("Stats"))},toString:function(){return"ScatterplotView()"}}),ScatterplotModel=Visualization.extend({defaults:{type:"scatterplot",config:{pagination:{currPage:0,perPage:3e3},width:400,height:400,margin:{top:16,right:16,bottom:40,left:54},xTicks:10,xLabel:"X",yTicks:10,yLabel:"Y",datapointSize:4,animDuration:500,scale:1,translate:[0,0]}}}); diff --git a/lib/galaxy/auth/__init__.py b/lib/galaxy/auth/__init__.py index 116229659a1..7dbb843b37f 100644 --- a/lib/galaxy/auth/__init__.py +++ b/lib/galaxy/auth/__init__.py @@ -20,19 +20,24 @@ log = logging.getLogger(__name__) # # -# activedirectory -# '[username]'.endswith('@students.latrobe.edu.au') +# ldap +# '[login]'.endswith('@students.latrobe.edu.au') # # True # ldap://STUDENTS.ltu.edu.au -# [(&(objectClass=user)(mail={username})) +# [(&(objectClass=user)(mail={login})) # dc=STUDENTS,dc=ltu,dc=edu,dc=au +# # jsmith # mysecret -# sAMAccountName] +# sAMAccountName,mail] # {sAMAccountName}@STUDENTS.ltu.edu.au # {password} # {sAMAccountName} +# {mail} +# # # # ... @@ -44,7 +49,7 @@ class AuthManager(object): def __init__(self, app): self.__app = app import galaxy.auth.providers - self.__plugins_dict = plugin_config.plugins_dict( galaxy.auth.providers, 'plugin_type' ) + self.__plugins_dict = plugin_config.plugins_dict(galaxy.auth.providers, 'plugin_type' ) auth_config_file = app.config.auth_config_file self.__init_authenticators(auth_config_file) @@ -80,18 +85,18 @@ class AuthManager(object): authenticators.append(authenticator) self.authenticators = authenticators - def check_registration_allowed(self, email, password): - """Checks if the provided email is allowed to register.""" + def check_registration_allowed(self, login, password): + """Checks if the provided email/username is allowed to register.""" message = '' status = 'done' - for provider, options in self.active_authenticators(email, password): + for provider, options in self.active_authenticators(login, password): allow_reg = _get_tri_state(options, 'allow-register', True) if allow_reg is None: # i.e. challenge - auth_result, msg = provider.authenticate(email, password, options) + auth_result, msg = provider.authenticate(login, password, options) if auth_result is True: break if auth_result is None: - message = 'Invalid email address or password' + message = 'Invalid email address/username or password' status = 'error' break elif allow_reg is True: @@ -102,16 +107,17 @@ class AuthManager(object): break return message, status - def check_auto_registration(self, trans, email, password): + def check_auto_registration(self, trans, login, password): """ - Checks the email/password using auth providers in order. If a match is - found, returns the 'auto-register' option for that provider. + Checks the username/email & password using auth providers in order. + If a match is found, returns the 'auto-register' option for that provider. """ - for provider, options in self.active_authenticators(email, password): + for provider, options in self.active_authenticators(login, password): if provider is None: log.debug( "Unable to find module: %s" % options ) else: - auth_result, auto_username = provider.authenticate(email, password, options) + auth_result, auto_email, auto_username = provider.authenticate(login, password, options) + auto_email = str(auto_email).lower() auto_username = str(auto_username).lower() if auth_result is True: # make username unique @@ -124,16 +130,16 @@ class AuthManager(object): i += 1 else: break # end for loop if we can't make a unique username - log.debug( "Email: %s, auto-register with username: %s" % (email, auto_username) ) - return (_get_bool(options, 'auto-register', False), auto_username) + log.debug( "Email: %s, auto-register with username: %s" % (auto_email, auto_username) ) + return (_get_bool(options, 'auto-register', False), auto_email, auto_username) elif auth_result is None: - log.debug( "Email: %s, stopping due to failed non-continue" % (email) ) + log.debug( "Email: %s, Username %s, stopping due to failed non-continue" % (auto_email, auto_username) ) break # end authentication (skip rest) return (False, '') def check_password(self, user, password): - """Checks the email/password using auth providers.""" - for provider, options in self.active_authenticators(user.email, password): + """Checks the username/email and password using auth providers.""" + for provider, options in self.active_authenticators(user, password): if provider is None: log.debug( "Unable to find module: %s" % options ) else: @@ -148,7 +154,7 @@ class AuthManager(object): """Checks that auth provider allows password changes and current_password matches. """ - for provider, options in self.active_authenticators(user.email, current_password): + for provider, options in self.active_authenticators(user, current_password): if provider is None: log.debug( "Unable to find module: %s" % options ) else: @@ -162,7 +168,7 @@ class AuthManager(object): return (False, 'Password change not supported') return (False, 'Invalid current password') - def active_authenticators(self, username, password): + def active_authenticators(self, login, password): """Yields AuthProvider instances for the provided configfile that match the filters. """ @@ -170,7 +176,7 @@ class AuthManager(object): for authenticator in self.authenticators: filter_template = authenticator.filter_template if filter_template: - filter_str = filter_template.format(username=username, password=password) + filter_str = filter_template.format(login=login, password=password) passed_filter = eval(filter_str, {"__builtins__": None}, {'str': str}) if not passed_filter: continue # skip to next diff --git a/lib/galaxy/auth/providers/__init__.py b/lib/galaxy/auth/providers/__init__.py index 724192007ce..fd200c01db4 100644 --- a/lib/galaxy/auth/providers/__init__.py +++ b/lib/galaxy/auth/providers/__init__.py @@ -16,23 +16,23 @@ class AuthProvider(object): """ Short string providing labelling this plugin """ @abc.abstractmethod - def authenticate(self, username, password, options): + def authenticate(self, login, password, options): """ Check that the username and password are correct. NOTE: Used within auto-registration to check it is ok to register this user. - :param username: the users email address - :type username: str + :param login: the user's email address or username + :type login: str :param password: the plain text password they typed :type password: str :param options: options provided in auth_config_file :type options: dict :returns: True: accept user, False: reject user and None: reject user - and don't try any other providers. str is the username to register - with if accepting - :rtype: (bool, str) + and don't try any other providers. str, str is the email and + username to register with if accepting + :rtype: (bool, str, str) """ @abc.abstractmethod @@ -44,7 +44,7 @@ class AuthProvider(object): NOTE: used on normal login to check authentication and update user details if required. - :param username: the users email address + :param username: the user's email address or username :type username: str :param password: the plain text password they typed :type password: str diff --git a/lib/galaxy/auth/providers/alwaysreject.py b/lib/galaxy/auth/providers/alwaysreject.py index a9cb54faf94..a2c27b6293a 100644 --- a/lib/galaxy/auth/providers/alwaysreject.py +++ b/lib/galaxy/auth/providers/alwaysreject.py @@ -16,11 +16,11 @@ class AlwaysReject(AuthProvider): """ plugin_type = 'alwaysreject' - def authenticate(self, username, password, options): + def authenticate(self, login, password, options): """ See abstract method documentation. """ - return (None, '') + return (None, '', '') def authenticate_user(self, user, password, options): """ diff --git a/lib/galaxy/auth/providers/activedirectory.py b/lib/galaxy/auth/providers/ldap_ad.py similarity index 52% rename from lib/galaxy/auth/providers/activedirectory.py rename to lib/galaxy/auth/providers/ldap_ad.py index 8f77a9703aa..ef2dddb83b8 100644 --- a/lib/galaxy/auth/providers/activedirectory.py +++ b/lib/galaxy/auth/providers/ldap_ad.py @@ -4,34 +4,37 @@ Created on 15/07/2014 @author: Andrew Robinson """ +import logging + from galaxy.exceptions import ConfigurationError from ..providers import AuthProvider +from galaxy.auth import _get_bool -import logging log = logging.getLogger(__name__) def _get_subs(d, k, params): if k not in d: - raise ConfigurationError("Missing '%s' parameter in Active Directory options" % k) + raise ConfigurationError("Missing '%s' parameter in LDAP options" % k) return str(d[k]).format(**params) -class ActiveDirectory(AuthProvider): - """ - Attempts to authenticate users against an Active Directory server. +class LDAP(AuthProvider): - If options include search-fields then it will attempt to search the AD for - those fields first. After that it will bind to the AD with the username + """ + Attempts to authenticate users against an LDAP server. + + If options include search-fields then it will attempt to search LDAP for + those fields first. After that it will bind to LDAP with the username (formatted as specified). """ - plugin_type = 'activedirectory' + plugin_type = 'ldap' - def authenticate(self, username, password, options): + def authenticate(self, login, password, options): """ See abstract method documentation. """ - log.debug("Username: %s" % username) + log.debug("Login: %s" % login) log.debug("Options: %s" % options) failure_mode = False # reject but continue @@ -41,11 +44,12 @@ class ActiveDirectory(AuthProvider): try: import ldap except: - log.debug("User: %s, ACTIVEDIRECTORY: False (no ldap)" % (username)) + log.debug( + "Login: %s, LDAP: False (could not load ldap module)" % (login)) return (failure_mode, '') - # do AD search (if required) - params = {'username': username, 'password': password} + # do LDAP search (if required) + params = {'login': login, 'password': password} if 'search-fields' in options: try: # setup connection @@ -54,21 +58,24 @@ class ActiveDirectory(AuthProvider): l.protocol_version = 3 if 'search-user' in options: - l.simple_bind_s(_get_subs(options, 'search-user', params), _get_subs(options, 'search-password', params)) + l.simple_bind_s(_get_subs(options, 'search-user', params), + _get_subs(options, 'search-password', params)) else: l.simple_bind_s() scope = ldap.SCOPE_SUBTREE # setup search - attributes = [_.strip().format(**params) for _ in options['search-fields'].split(',')] - result = l.search(_get_subs(options, 'search-base', params), scope, _get_subs(options, 'search-filter', params), attributes) + attributes = [_.strip().format(**params) + for _ in options['search-fields'].split(',')] + result = l.search(_get_subs(options, 'search-base', params), scope, + _get_subs(options, 'search-filter', params), attributes) # parse results _, suser = l.result(result, 60) dn, attrs = suser[0] - log.debug(("AD dn: %s" % dn)) - log.debug(("AD Search attributes: %s" % attrs)) + log.debug(("LDAP dn: %s" % dn)) + log.debug(("LDAP Search attributes: %s" % attrs)) if hasattr(attrs, 'has_key'): for attr in attributes: if attr in attrs: @@ -77,8 +84,8 @@ class ActiveDirectory(AuthProvider): params[attr] = "" params['dn'] = dn except Exception: - log.exception('ACTIVEDIRECTORY Search Exception for User: %s' % username) - return (failure_mode, '') + log.exception('LDAP Search Exception for login: %s' % login) + return (failure_mode, '', '') # end search # bind as user to check their credentials @@ -87,19 +94,31 @@ class ActiveDirectory(AuthProvider): ldap.set_option(ldap.OPT_REFERRALS, 0) l = ldap.initialize(_get_subs(options, 'server', params)) l.protocol_version = 3 - l.simple_bind_s(_get_subs(options, 'bind-user', params), _get_subs(options, 'bind-password', params)) + l.simple_bind_s(_get_subs( + options, 'bind-user', params), _get_subs(options, 'bind-password', params)) except Exception: - log.exception('ACTIVEDIRECTORY Authenticate Exception for User %s' % username) - return (failure_mode, '') + log.exception('LDAP Authentication Exception for login %s' % login) + return (failure_mode, '', '') - log.debug("User: %s, ACTIVEDIRECTORY: True" % (username)) - return (True, _get_subs(options, 'auto-register-username', params)) + log.debug("Login: %s, LDAP: True" % (login)) + return (True, + _get_subs(options, 'auto-register-email', params), + _get_subs(options, 'auto-register-username', params)) def authenticate_user(self, user, password, options): """ See abstract method documentation. """ - return self.authenticate(user.email, password, options)[0] + + if _get_bool(options, 'login-use-username', False): + return self.authenticate(user.username, password, options)[0] + else: + return self.authenticate(user.email, password, options)[0] -__all__ = ['ActiveDirectory'] +class ActiveDirectory(LDAP): + """ Effectively just an alias for LDAP auth, but may contain active directory specific + logic in the future. """ + plugin_type = 'activedirectory' + +__all__ = ['LDAP', 'ActiveDirectory'] diff --git a/lib/galaxy/auth/providers/localdb.py b/lib/galaxy/auth/providers/localdb.py index 5f01e6e5208..718889acad0 100644 --- a/lib/galaxy/auth/providers/localdb.py +++ b/lib/galaxy/auth/providers/localdb.py @@ -13,11 +13,11 @@ class LocalDB(AuthProvider): """Authenticate users against the local Galaxy database (as per usual).""" plugin_type = 'localdb' - def authenticate(self, username, password, options): + def authenticate(self, login, password, options): """ See abstract method documentation. """ - return (False, '') # it can never auto-create based of localdb (chicken-egg) + return (False, '', '') # it can never auto-create based of localdb (chicken-egg) def authenticate_user(self, user, password, options): """ diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index a7f9863d0ad..fc011af9fea 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -262,7 +262,7 @@ class Configuration( object ): self.message_box_content = kwargs.get( 'message_box_content', None ) self.message_box_class = kwargs.get( 'message_box_class', 'info' ) self.support_url = kwargs.get( 'support_url', 'https://wiki.galaxyproject.org/Support' ) - self.wiki_url = kwargs.get( 'wiki_url', 'http://wiki.galaxyproject.org/' ) + self.wiki_url = kwargs.get( 'wiki_url', 'https://wiki.galaxyproject.org/' ) self.blog_url = kwargs.get( 'blog_url', None ) self.screencasts_url = kwargs.get( 'screencasts_url', None ) self.library_import_dir = kwargs.get( 'library_import_dir', None ) @@ -302,6 +302,8 @@ class Configuration( object ): self.nginx_x_archive_files_base = kwargs.get( 'nginx_x_archive_files_base', False ) self.nginx_upload_store = kwargs.get( 'nginx_upload_store', False ) self.nginx_upload_path = kwargs.get( 'nginx_upload_path', False ) + self.nginx_upload_job_files_store = kwargs.get( 'nginx_upload_job_files_store', False ) + self.nginx_upload_job_files_path = kwargs.get( 'nginx_upload_job_files_path', False ) if self.nginx_upload_store: self.nginx_upload_store = os.path.abspath( self.nginx_upload_store ) self.object_store = kwargs.get( 'object_store', 'disk' ) @@ -356,6 +358,8 @@ class Configuration( object ): # Default URL (with schema http/https) of the Galaxy instance within the # local network - used to remotely communicate with the Galaxy API. + web_port = kwargs.get("galaxy_infrastructure_web_port", None) + self.galaxy_infrastructure_web_port = web_port galaxy_infrastructure_url = kwargs.get( 'galaxy_infrastructure_url', None ) galaxy_infrastructure_url_set = True if galaxy_infrastructure_url is None: @@ -363,9 +367,9 @@ class Configuration( object ): # so dependending on the context a better default can be used ( # request url in a web thread, Docker parent in IE stuff, etc...) galaxy_infrastructure_url = "http://localhost" - port = self.guess_galaxy_port() - if port: - galaxy_infrastructure_url += ":%s" % (port) + web_port = self.galaxy_infrastructure_web_port or self.guess_galaxy_port() + if web_port: + galaxy_infrastructure_url += ":%s" % (web_port) galaxy_infrastructure_url_set = False if "HOST_IP" in galaxy_infrastructure_url: galaxy_infrastructure_url = string.Template(galaxy_infrastructure_url).safe_substitute({ diff --git a/lib/galaxy/datatypes/binary.py b/lib/galaxy/datatypes/binary.py index 4fe54baabff..3d6f6e0e1e4 100644 --- a/lib/galaxy/datatypes/binary.py +++ b/lib/galaxy/datatypes/binary.py @@ -24,6 +24,9 @@ from galaxy.datatypes.metadata import MetadataElement, MetadataParameter, ListPa from galaxy.datatypes import metadata import dataproviders +eggs.require( "pysam" ) +from pysam import csamtools + log = logging.getLogger(__name__) # Currently these supported binary data types must be manually set on upload @@ -146,6 +149,12 @@ class Bam( Binary ): data_sources = { "data": "bai", "index": "bigwig" } MetadataElement( name="bam_index", desc="BAM Index File", param=metadata.FileParameter, file_ext="bai", readonly=True, no_value=None, visible=False, optional=True ) + MetadataElement( name="bam_version", default=None, desc="BAM Version", param=MetadataParameter, readonly=True, visible=False, optional=True, no_value=None ) + MetadataElement( name="sort_order", default=None, desc="Sort Order", param=MetadataParameter, readonly=True, visible=False, optional=True, no_value=None ) + MetadataElement( name="read_groups", default=[], desc="Read Groups", param=MetadataParameter, readonly=True, visible=False, optional=True, no_value=[] ) + MetadataElement( name="reference_names", default=[], desc="Chromosome Names", param=MetadataParameter, readonly=True, visible=False, optional=True, no_value=[] ) + MetadataElement( name="reference_lengths", default=[], desc="Chromosome Lengths", param=MetadataParameter, readonly=True, visible=False, optional=True, no_value=[] ) + MetadataElement( name="bam_header", default={}, desc="Dictionary of BAM Headers", param=MetadataParameter, readonly=True, visible=False, optional=True, no_value={} ) def _get_samtools_version( self ): # Determine the version of samtools being used. Wouldn't it be nice if @@ -291,6 +300,17 @@ class Bam( Binary ): dataset.metadata.bam_index = index_file # Remove temp file os.unlink( stderr_name ) + # Now use pysam with BAI index to determine additional metadata + try: + bam_file = csamtools.Samfile( filename=dataset.file_name, mode='rb', index_filename=index_file.file_name ) + dataset.metadata.reference_names = list( bam_file.references ) + dataset.metadata.reference_lengths = list( bam_file.lengths ) + dataset.metadata.bam_header = bam_file.header + dataset.metadata.read_groups = [ read_group['ID'] for read_group in dataset.metadata.bam_header.get( 'RG', [] ) if 'ID' in read_group ] + dataset.metadata.sort_order = dataset.metadata.bam_header.get( 'HD', {} ).get( 'SO', None ) + dataset.metadata.bam_version = dataset.metadata.bam_header.get( 'HD', {} ).get( 'VN', None ) + except: + pass def sniff( self, filename ): # BAM is compressed in the BGZF format, and must not be uncompressed in Galaxy. @@ -753,3 +773,40 @@ class Xlsx(Binary): Binary.register_sniffable_binary_format("xlsx", "xlsx", Xlsx) + +class Sra( Binary ): + """ Sequence Read Archive (SRA) datatype originally from mdshw5/sra-tools-galaxy""" + file_ext = 'sra' + + def __init__( self, **kwd ): + Binary.__init__( self, **kwd ) + + def sniff( self, filename ): + """ The first 8 bytes of any NCBI sra file is 'NCBI.sra', and the file is binary. + For details about the format, see http://www.ncbi.nlm.nih.gov/books/n/helpsra/SRA_Overview_BK/#SRA_Overview_BK.4_SRA_Data_Structure + """ + try: + header = open(filename).read(8) + if binascii.b2a_hex(header) == binascii.hexlify('NCBI.sra'): + return True + else: + return False + except: + return False + + def set_peek(self, dataset, is_multi_byte=False): + if not dataset.dataset.purged: + dataset.peek = 'Binary sra file' + dataset.blurb = data.nice_size(dataset.get_size()) + else: + dataset.peek = 'file does not exist' + dataset.blurb = 'file purged from disk' + + def display_peek(self, dataset): + try: + return dataset.peek + except: + return 'Binary sra file (%s)' % (data.nice_size(dataset.get_size())) + +Binary.register_sniffable_binary_format('sra', 'sra', Sra) + diff --git a/lib/galaxy/external_services/service.py b/lib/galaxy/external_services/service.py index 2bc11848162..fe6fa1ce628 100644 --- a/lib/galaxy/external_services/service.py +++ b/lib/galaxy/external_services/service.py @@ -210,19 +210,5 @@ class PopulatedExternalService( object ): assert action, 'Action not found: %s' % actions_list return action - def get_action_links( self ): - rval = [] - param_dict = {} - param_dict['fields'] = Bunch( **self.service_instance.form_values.content ) - param_dict['item'] = self.item - for item in self.service.items: - if isinstance( item, ExternalServiceParameter ): - param_dict[ item.name ] = item.get_value( param_dict ) - elif isinstance( item, ExternalServiceAction ): - rval.append( item.get_action_access_link( self.item, trans, param_dict ) ) - elif isinstance( item, ExternalServiceActionsGroup ): - rval.extend( item.populate( self.service_instance, item, param_dict ).get_action_links() ) - else: - raise 'unknown item type found' def __nonzero__( self ): return self.actions.has_action() diff --git a/lib/galaxy/jobs/runners/pulsar.py b/lib/galaxy/jobs/runners/pulsar.py index f1da51ff963..f53de275187 100644 --- a/lib/galaxy/jobs/runners/pulsar.py +++ b/lib/galaxy/jobs/runners/pulsar.py @@ -32,6 +32,8 @@ __all__ = [ 'PulsarLegacyJobRunner', 'PulsarRESTJobRunner', 'PulsarMQJobRunner' NO_REMOTE_GALAXY_FOR_METADATA_MESSAGE = "Pulsar misconfiguration - Pulsar client configured to set metadata remotely, but remote Pulsar isn't properly configured with a galaxy_home directory." NO_REMOTE_DATATYPES_CONFIG = "Pulsar client is configured to use remote datatypes configuration when setting metadata externally, but Pulsar is not configured with this information. Defaulting to datatypes_conf.xml." GENERIC_REMOTE_ERROR = "Failed to communicate with remote job server." +FAILED_REMOTE_ERROR = "Remote job server indicated a problem running or monitoring this job." +LOST_REMOTE_ERROR = "Remote job server could not determine this job's state." # Is there a good way to infer some default for this? Can only use # url_for from web threads. https://gist.github.com/jmchilton/9098762 @@ -170,8 +172,13 @@ class PulsarJobRunner( AsynchronousJobRunner ): if pulsar_status == "complete": self.mark_as_finished(job_state) return None - if pulsar_status == "failed": - self.fail_job(job_state) + if pulsar_status in ["failed", "lost"]: + if pulsar_status == "failed": + message = FAILED_REMOTE_ERROR + else: + message = LOST_REMOTE_ERROR + if not job_state.job_wrapper.get_job().finished: + self.fail_job(job_state, message) return None if pulsar_status == "running" and not job_state.running: job_state.running = True @@ -342,7 +349,12 @@ class PulsarJobRunner( AsynchronousJobRunner ): encoded_job_id = self.app.security.encode_id(job_id) job_key = self.app.security.encode_id( job_id, kind="jobs_files" ) - files_endpoint = "%s/api/jobs/%s/files?job_key=%s" % ( + endpoint_base = "%s/api/jobs/%s?job_key=%s" + if self.app.config.nginx_upload_job_files_path: + endpoint_base = "%s" + \ + self.app.config.nginx_upload_job_files_path + \ + "?job_id=%s&job_key=%s" + files_endpoint = endpoint_base % ( self.galaxy_url, encoded_job_id, job_key @@ -398,12 +410,12 @@ class PulsarJobRunner( AsynchronousJobRunner ): log.exception("Job wrapper finish method failed") job_wrapper.fail("Unable to finish job", exception=True) - def fail_job( self, job_state ): + def fail_job( self, job_state, message=GENERIC_REMOTE_ERROR ): """ Seperated out so we can use the worker threads for it. """ self.stop_job( self.sa_session.query( self.app.model.Job ).get( job_state.job_wrapper.job_id ) ) - job_state.job_wrapper.fail( getattr( job_state, "fail_message", GENERIC_REMOTE_ERROR ) ) + job_state.job_wrapper.fail( getattr( job_state, "fail_message", message ) ) def check_pid( self, pid ): try: diff --git a/lib/galaxy/managers/annotatable.py b/lib/galaxy/managers/annotatable.py index 0b1e4332cc0..af2dfbac802 100644 --- a/lib/galaxy/managers/annotatable.py +++ b/lib/galaxy/managers/annotatable.py @@ -12,24 +12,24 @@ class AnnotatableManagerMixin( object ): # TODO: most of this seems to be covered by item_attrs.UsesAnnotations # TODO: use these below (serializer/deserializer) - def user_annotation( self, trans, item, user ): - return item.get_item_annotation_str( self.app.model.context, user, item ) + def user_annotation( self, item, user ): + return item.get_item_annotation_str( self.session(), user, item ) - def owner_annotation( self, trans, item ): - return self.user_annotation( trans, item, item.user ) + def owner_annotation( self, item ): + return self.user_annotation( item, item.user ) - def delete_annotation( self, trans, item, user ): - return item.delete_item_annotation( self.app.model.context, user, item ) + def delete_annotation( self, item, user ): + return item.delete_item_annotation( self.session(), user, item ) - def annotate( self, trans, item, user, annotation ): + def annotate( self, item, user, annotation ): if annotation is None: - self.delete_annotation( self, trans, item, user ) + self.delete_annotation( item, user ) return None - annotation_obj = item.add_item_annotation( self.app.model.context, user, item, annotation ) + annotation_obj = item.add_item_annotation( self.session(), user, item, annotation ) return annotation_obj.annotation - #def by_user( self, trans, user, **kwargs ): + #def by_user( self, user, **kwargs ): # pass @@ -38,13 +38,11 @@ class AnnotatableSerializerMixin( object ): def add_serializers( self ): self.serializers[ 'annotation' ] = self.serialize_annotation - def serialize_annotation( self, trans, item, key ): + def serialize_annotation( self, item, key, user=None, **context ): """ Get and serialize an `item`'s annotation. """ # user = item.user - #TODO: trans - user = trans.user sa_session = self.app.model.context returned = item.get_item_annotation_str( sa_session, user, item ) return returned @@ -55,19 +53,39 @@ class AnnotatableDeserializerMixin( object ): def add_deserializers( self ): self.deserializers[ 'annotation' ] = self.deserialize_annotation - def deserialize_annotation( self, trans, item, key, val ): + def deserialize_annotation( self, item, key, val, user=None, **context ): """ Make sure `val` is a valid annotation and assign it, deleting any existing if `val` is None. """ val = self.validate.nullable_basestring( key, val ) + return self.manager.annotate( item, user, val ) - sa_session = self.app.model.context - #TODO: trans - user = trans.user - if val is None: - item.delete_item_annotation( sa_session, user, item ) + +# TODO: I'm not entirely convinced this (or tags) are a good idea for filters since they involve a/the user +class AnnotatableFilterMixin( object ): + + def _owner_annotation( self, item ): + """ + Get the annotation by the item's owner. + """ + if not item.user: return None + for annotation in item.annotations: + if annotation.user == item.user: + return annotation.annotation + return None - annotated_item = item.add_item_annotation( sa_session, user, item, val ) - return annotated_item.annotation + def filter_annotation_contains( self, item, val ): + """ + Test whether `val` is in the owner's annotation. + """ + owner_annotation = self._owner_annotation( item ) + if owner_annotation is None: + return False + return val in owner_annotation + + def _add_parsers( self ): + self.fn_filter_parsers.update({ + 'annotation' : { 'op': { 'has': self.filter_annotation_contains, } }, + }) diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py index 8c81223e6f9..c93063db519 100644 --- a/lib/galaxy/managers/base.py +++ b/lib/galaxy/managers/base.py @@ -179,7 +179,7 @@ class ModelManager( object ): return item # .... query foundation wrapper - def query( self, trans, eagerloads=True, filters=None, order_by=None, limit=None, offset=None, **kwargs ): + def query( self, eagerloads=True, filters=None, order_by=None, limit=None, offset=None, **kwargs ): """ Return a basic query from model_class, filters, order_by, and limit and offset. @@ -258,11 +258,11 @@ class ModelManager( object ): return query # .... query resolution - def one( self, trans, **kwargs ): + def one( self, **kwargs ): """ Sends kwargs to build the query and returns one and only one model. """ - query = self.query( trans, **kwargs ) + query = self.query( **kwargs ) return self._one_with_recast_errors( query ) def _one_with_recast_errors( self, query ): @@ -292,33 +292,33 @@ class ModelManager( object ): return None #NOTE: at this layer, all ids are expected to be decoded and in int form - def by_id( self, trans, id, **kwargs ): + def by_id( self, id, **kwargs ): """ Gets a model by primary id. """ id_filter = self.model_class.id == id - return self.one( trans, filters=id_filter, **kwargs ) + return self.one( filters=id_filter, **kwargs ) # .... multirow queries - def _orm_list( self, trans, query=None, **kwargs ): + def _orm_list( self, query=None, **kwargs ): """ Sends kwargs to build the query return all models found. """ - query = query or self.query( trans, **kwargs ) + query = query or self.query( **kwargs ) return query.all() - #def list( self, trans, query=None, filters=None, order_by=None, limit=None, offset=None, **kwargs ): - def list( self, trans, filters=None, order_by=None, limit=None, offset=None, **kwargs ): + #def list( self, query=None, filters=None, order_by=None, limit=None, offset=None, **kwargs ): + def list( self, filters=None, order_by=None, limit=None, offset=None, **kwargs ): """ Returns all objects matching the given filters """ orm_filters, fn_filters = self._split_filters( filters ) if not fn_filters: # if no fn_filtering required, we can use the 'all orm' version with limit offset - return self._orm_list( trans, filters=orm_filters, order_by=order_by, limit=limit, offset=offset, **kwargs ) + return self._orm_list( filters=orm_filters, order_by=order_by, limit=limit, offset=offset, **kwargs ) # fn filters will change the number of items returnable by limit/offset - remove them here from the orm query - query = self.query( trans, filters=orm_filters, order_by=order_by, limit=None, offset=None, **kwargs ) + query = self.query( filters=orm_filters, order_by=order_by, limit=None, offset=None, **kwargs ) items = query.all() # apply limit, offset after SQL filtering @@ -381,12 +381,12 @@ class ModelManager( object ): yield item yielded += 1 - def by_ids( self, trans, ids, filters=None, **kwargs ): + def by_ids( self, ids, filters=None, **kwargs ): """ Returns an in-order list of models with the matching ids in `ids`. """ ids_filter = self.model_class.id.in_( ids ) - found = self.list( trans, filters=self._munge_filters( ids_filter, filters ), **kwargs ) + found = self.list( filters=self._munge_filters( ids_filter, filters ), **kwargs ) # TODO: this does not order by the original 'ids' array # ...could use get (supposedly since found are in the session, the db won't be hit twice) @@ -420,7 +420,7 @@ class ModelManager( object ): in_order.append( item_dict[ id ] ) return in_order - def create( self, trans, flush=True, *args, **kwargs ): + def create( self, flush=True, *args, **kwargs ): """ Generically create a new model. """ @@ -431,13 +431,13 @@ class ModelManager( object ): self.session().flush() return item - def copy( self, trans, item, **kwargs ): + def copy( self, item, **kwargs ): """ Clone or copy an item. """ raise exceptions.NotImplemented( 'Abstract method' ) - def update( self, trans, item, new_values, flush=True, **kwargs ): + def update( self, item, new_values, flush=True, **kwargs ): """ Given a dictionary of new values, update `item` and return it. @@ -452,7 +452,7 @@ class ModelManager( object ): return item # TODO: yagni? - def associate( self, trans, associate_with, item, foreign_key_name=None ): + def associate( self, associate_with, item, foreign_key_name=None ): """ Generically associate `item` with `associate_with` based on `foreign_key_name`. """ @@ -460,7 +460,7 @@ class ModelManager( object ): setattr( associate_with, foreign_key_name, item ) return item - def query_associated( self, trans, associated_model_class, item, foreign_key_name=None ): + def query_associated( self, associated_model_class, item, foreign_key_name=None ): """ Generically query other items that have been associated with this `item`. """ @@ -469,7 +469,7 @@ class ModelManager( object ): return self.session().query( associated_model_class ).filter( foreign_key == item ) # a rename of sql DELETE to differentiate from the Galaxy notion of mark_as_deleted - # def destroy( self, trans, item, **kwargs ): + # def destroy( self, item, **kwargs ): # return item @@ -486,13 +486,21 @@ class ModelDeserializingError( exceptions.ObjectAttributeInvalidException ): pass +class SkipAttribute( Exception ): + """ + Raise this inside a serializer to prevent the returned dictionary from having + a the associated key or value for this attribute. + """ + pass + + class ModelSerializer( object ): """ Turns models into JSONable dicts. Maintains a map of requestable keys and the Callable() serializer functions that should be called for those keys. - E.g. { 'x' : lambda trans, item, key: item.x, ... } + E.g. { 'x' : lambda item, key: item.x, ... } Note: if a key to serialize is not listed in the Serializer.serializable_keyset or serializers, it will not be returned. @@ -501,7 +509,7 @@ class ModelSerializer( object ): my_serializer = MySerializer( app ) ... keys_to_serialize = [ 'id', 'name', 'attr1', 'attr2', ... ] - item_dict = MySerializer.serialize( trans, my_item, keys_to_serialize ) + item_dict = MySerializer.serialize( my_item, keys_to_serialize ) """ #: 'service' to use for getting urls - use class var to allow overriding when testing url_for = staticmethod( routes.url_for ) @@ -534,8 +542,9 @@ class ModelSerializer( object ): Register a map of attribute keys -> serializing functions that will serialize the attribute. """ - # to be overridden in subclasses - pass + self.serializers.update({ + 'id' : self.serialize_id, + }) def add_view( self, view_name, key_list, include_keys_from=None ): """ @@ -550,7 +559,7 @@ class ModelSerializer( object ): self.serializable_keyset.update( key_list ) return key_list - def serialize( self, trans, item, keys ): + def serialize( self, item, keys, **context ): """ Serialize the model `item` to a dictionary. @@ -558,18 +567,37 @@ class ModelSerializer( object ): built from each key in `keys` that also exists in `serializers` and values of calling the keyed/named serializers on item. """ - # main interface fn for converting a model to a dict + # TODO: constrain context to current_user/whos_asking when that's all we need (trans) returned = {} for key in keys: # check both serializers and serializable keys if key in self.serializers: - returned[ key ] = self.serializers[ key ]( trans, item, key ) + try: + returned[ key ] = self.serializers[ key ]( item, key, **context ) + except SkipAttribute, skip: + # dont add this key if the deserializer threw this + pass elif key in self.serializable_keyset: - returned[ key ] = self.default_serializer( trans, item, key ) + returned[ key ] = self.default_serializer( item, key, **context ) # ignore bad/unreg keys return returned - def default_serializer( self, trans, item, key ): + def skip( self, msg='skipped' ): + """ + To be called from inside a serializer to skip it. + + Handy for config checks, information hiding, etc. + """ + raise SkipAttribute( msg ) + + def _remap_from( self, original_key ): + if original_key in self.serializers: + return self.serializers[ original_key ] + if original_key in self.serializable_keyset: + return lambda i, k, **c: self.default_serializer( i, original_key, **c ) + raise KeyError( 'serializer not found for remap: ' + original_key ) + + def default_serializer( self, item, key, **context ): """ Serialize the `item`'s attribute named `key`. """ @@ -577,22 +605,23 @@ class ModelSerializer( object ): return getattr( item, key ) # serializers for common galaxy objects - def serialize_date( self, trans, item, key ): + def serialize_date( self, item, key, **context ): """ Serialize a date attribute of `item`. """ date = getattr( item, key ) return date.isoformat() if date is not None else None - def serialize_id( self, trans, item, key ): + def serialize_id( self, item, key, **context ): """ Serialize an id attribute of `item`. """ id = getattr( item, key ) + # Note: it may not be best to encode the id at this layer return self.app.security.encode_id( id ) if id is not None else None # serializing to a view where a view is a predefied list of keys to serialize - def serialize_to_view( self, trans, item, view=None, keys=None, default_view=None ): + def serialize_to_view( self, item, view=None, keys=None, default_view=None, **context ): """ Use a predefined list of keys (the string `view`) and any additional keys listed in `keys`. @@ -603,7 +632,7 @@ class ModelSerializer( object ): no `view` or `keys`: use the `default_view` if any `view` and `keys`: combine both into one list of keys """ -#TODO: default view + view makes no sense outside the API.index context - move default view there + #TODO: default view + view makes no sense outside the API.index context - move default view there all_keys = [] keys = keys or [] # chose explicit over concise here @@ -618,7 +647,7 @@ class ModelSerializer( object ): elif default_view: all_keys = self._view_to_keys( default_view ) - return self.serialize( trans, item, all_keys ) + return self.serialize( item, all_keys, **context ) def _view_to_keys( self, view=None ): """ @@ -668,16 +697,17 @@ class ModelDeserializer( object ): # to be overridden in subclasses pass - def deserialize( self, trans, item, data, flush=True ): + def deserialize( self, item, data, flush=True, **context ): """ Convert an incoming serialized dict into values that can be directly assigned to an item's attributes and assign them """ + # TODO: constrain context to current_user/whos_asking when that's all we need (trans) sa_session = self.app.model.context new_dict = {} for key, val in data.items(): if key in self.deserializers: - new_dict[ key ] = self.deserializers[ key ]( trans, item, key, val ) + new_dict[ key ] = self.deserializers[ key ]( item, key, val, **context ) # !important: don't error on unreg. keys -- many clients will add weird ass keys onto the model # TODO:?? add and flush here or in manager? @@ -688,7 +718,7 @@ class ModelDeserializer( object ): return new_dict # ... common deserializers for primitives - def default_deserializer( self, trans, item, key, val ): + def default_deserializer( self, item, key, val, **context ): """ If the incoming `val` is different than the `item` value change it and, in either case, return the value. @@ -700,28 +730,28 @@ class ModelDeserializer( object ): setattr( item, key, val ) return val - def deserialize_basestring( self, trans, item, key, val, convert_none_to_empty=False ): + def deserialize_basestring( self, item, key, val, convert_none_to_empty=False, **context ): val = '' if ( convert_none_to_empty and val is None ) else self.validate.basestring( key, val ) - return self.default_deserializer( trans, item, key, val ) + return self.default_deserializer( item, key, val, **context ) - def deserialize_bool( self, trans, item, key, val ): + def deserialize_bool( self, item, key, val, **context ): val = self.validate.bool( key, val ) - return self.default_deserializer( trans, item, key, val ) + return self.default_deserializer( item, key, val, **context ) - def deserialize_int( self, trans, item, key, val, min=None, max=None ): + def deserialize_int( self, item, key, val, min=None, max=None, **context ): val = self.validate.int_range( key, val, min, max ) - return self.default_deserializer( trans, item, key, val ) + return self.default_deserializer( item, key, val, **context ) - #def deserialize_date( self, trans, item, key, val ): + #def deserialize_date( self, item, key, val ): # #TODO: parse isoformat date into date object # ... common deserializers for Galaxy - def deserialize_genome_build( self, trans, item, key, val ): + def deserialize_genome_build( self, item, key, val, **context ): """ Make sure `val` is a valid dbkey and assign it. """ val = self.validate.genome_build( key, val ) - return self.default_deserializer( trans, item, key, val ) + return self.default_deserializer( item, key, val, **context ) class ModelValidator( object ): @@ -801,7 +831,7 @@ class ModelValidator( object ): # IOW: fallback to string validation return self.basestring( key, val ) - # def slug( self, trans, item, key, val ): + # def slug( self, item, key, val ): # """validate slug""" # pass @@ -854,9 +884,11 @@ class ModelFilterParser( object ): """ Set up, extend, or alter `orm_filter_parsers` and `fn_filter_parsers`. """ + # note: these are the default filters for all models self.orm_filter_parsers.update({ # (prob.) applicable to all models - 'id' : { 'op': ( 'in' ), 'val': self.parse_id_list }, + 'id' : { 'op': ( 'in' ) }, + 'encoded_id' : { 'column' : 'id', 'op': ( 'in' ), 'val': self.parse_id_list }, # dates can be directly passed through the orm into a filter (no need to parse into datetime object) 'create_time' : { 'op': ( 'le', 'ge' ) }, 'update_time' : { 'op': ( 'le', 'ge' ) }, @@ -895,7 +927,7 @@ class ModelFilterParser( object ): # by convention, assume most val parsers raise ValueError except ValueError, val_err: raise exceptions.RequestParameterInvalidException( 'unparsable value for filter', - column=attr, operation=op, value=val ) + column=attr, operation=op, value=val, ValueError=str( val_err ) ) # if neither of the above work, raise an error with how-to info #TODO: send back all valid filter keys in exception for added user help @@ -904,6 +936,7 @@ class ModelFilterParser( object ): # ---- fn filters def _parse_fn_filter( self, attr, op, val ): """ + Attempt to parse a non-ORM filter function. """ # fn_filter_list is a dict: fn_filter_list[ attr ] = { 'opname1' : opfn1, 'opname2' : opfn2, etc. } @@ -927,18 +960,33 @@ class ModelFilterParser( object ): # ---- ORM filters def _parse_orm_filter( self, attr, op, val ): """ + Attempt to parse a ORM-based filter. + + Using SQLAlchemy, this would yield a sql.elements.BinaryExpression. """ # orm_filter_list is a dict: orm_filter_list[ attr ] = - # attr must be a whitelisted column - column = self.model_class.table.columns.get( attr ) column_map = self.orm_filter_parsers.get( attr, None ) - if column is None or not column_map: + if not column_map: + # no column mapping (not whitelisted) return None + # attr must be a whitelisted column by attr name or by key passed in column_map + # note: column_map[ 'column' ] takes precedence + column = self.model_class.table.columns.get( attr ) + if 'column' in column_map: + remap_to = column_map[ 'column' ] + column = self.model_class.table.columns.get( remap_to ) + if column is None: + # no orm column + return None + # op must be whitelisted: contained in the list orm_filter_list[ attr ][ 'op' ] allowed_ops = column_map.get( 'op' ) if op not in allowed_ops: return None op = self._convert_op_string_to_fn( column, op ) + if not op: + return None + # parse the val from string using the 'val' parser if present (otherwise, leave as string) val_parser = column_map.get( 'val', None ) if val_parser: @@ -963,65 +1011,23 @@ class ModelFilterParser( object ): fn_name = 'in_' # get the column fn using the op_string and error if not a callable attr - #TODO: special case 'not in' - or disallow? + # TODO: special case 'not in' - or disallow? op_fn = getattr( column, fn_name, None ) if not op_fn or not callable( op_fn ): return None return op_fn + # ---- preset fn_filters: dictionaries of standard filter ops for standard datatypes + def string_standard_ops( self, key ): + return { + 'op' : { + 'eq' : lambda i, v: v == getattr( i, key ), + 'contains' : lambda i, v: v in getattr( i, key ), + } + } + # --- more parsers! yay! -#TODO: These should go somewhere central - we've got ~6 parser modules/sections now - #TODO: to annotatable - def _owner_annotation( self, item ): - """ - Get the annotation by the item's owner. - """ - if not item.user: - return None - for annotation in item.annotations: - if annotation.user == item.user: - return annotation.annotation - return None - - def filter_annotation_contains( self, item, val ): - """ - Test whether `val` is in the owner's annotation. - """ - owner_annotation = self._owner_annotation( item ) - if owner_annotation is None: - return False - return val in owner_annotation - - #TODO: to taggable - def _tag_str_gen( self, item ): - """ - Return a list of strings built from the item's tags. - """ - #TODO: which user is this? all? - for tag in item.tags: - tag_str = tag.user_tname - if tag.value is not None: - tag_str += ":" + tag.user_value - yield tag_str - - def filter_has_partial_tag( self, item, val ): - """ - Return True if any tag partially contains `val`. - """ - for tag_str in self._tag_str_gen( item ): - if val in tag_str: - return True - return False - - def filter_has_tag( self, item, val ): - """ - Return True if any tag exactly equals `val`. - """ - for tag_str in self._tag_str_gen( item ): - if val == tag_str: - return True - return False - + #TODO: These should go somewhere central - we've got ~6 parser modules/sections now def parse_bool( self, bool_string ): """ Parse a boolean from a string. diff --git a/lib/galaxy/managers/collections.py b/lib/galaxy/managers/collections.py index ccdf0c16900..b129ff5d61e 100644 --- a/lib/galaxy/managers/collections.py +++ b/lib/galaxy/managers/collections.py @@ -277,7 +277,7 @@ class DatasetCollectionManager( object ): if src_type == 'hda': decoded_id = int( trans.app.security.decode_id( encoded_id ) ) - element = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) + element = self.hda_manager.get_accessible( decoded_id, trans.user ) elif src_type == 'ldda': element = self.ldda_manager.get( trans, encoded_id ) elif src_type == 'hdca': @@ -312,9 +312,9 @@ class DatasetCollectionManager( object ): instance_id = int( trans.app.security.decode_id( id ) ) collection_instance = trans.sa_session.query( trans.app.model.HistoryDatasetCollectionAssociation ).get( instance_id ) if check_ownership: - self.history_manager.error_unless_owner( trans, collection_instance.history, trans.user ) + self.history_manager.error_unless_owner( collection_instance.history, trans.user, current_history=trans.history ) if check_accessible: - self.history_manager.error_unless_accessible( trans, collection_instance.history, trans.user ) + self.history_manager.error_unless_accessible( collection_instance.history, trans.user, current_history=trans.history ) return collection_instance def __get_library_collection_instance( self, trans, id, check_ownership=False, check_accessible=True ): diff --git a/lib/galaxy/managers/configuration.py b/lib/galaxy/managers/configuration.py index b85b2f94c3f..81b22f5f9e0 100644 --- a/lib/galaxy/managers/configuration.py +++ b/lib/galaxy/managers/configuration.py @@ -15,6 +15,7 @@ import logging log = logging.getLogger( __name__ ) +#TODO: for lack of a manager file for the config. May well be better in config.py? Circ imports? class ConfigSerializer( base.ModelSerializer ): def __init__( self, app ): @@ -23,61 +24,64 @@ class ConfigSerializer( base.ModelSerializer ): self.default_view = 'all' self.add_view( 'all', self.serializers.keys() ) - def default_serializer( self, trans, config, key ): - return config.get( key, None ) - - def _defaults_to( self, default ): - return lambda t, i, k: i.get( k, default ) + def default_serializer( self, config, key ): + return getattr( config, key, None ) def add_serializers( self ): + def _defaults_to( default ): + return lambda i, k, **c: getattr( i, k, default ) + self.serializers = { #TODO: this is available from user data, remove - 'is_admin_user' : lambda *a: False, + 'is_admin_user' : lambda *a, **c: False, - 'brand' : lambda t, i, k: i.get( k, "" ), + 'brand' : _defaults_to( '' ), #TODO: this doesn't seem right - 'logo_url' : lambda t, i, k: web.url_for( i.get( k, '/') ), - 'terms_url' : lambda t, i, k: i.get( k, "" ), + 'logo_url' : lambda i, k, **c: self.url_for( i.get( k, '/' ) ), + 'terms_url' : _defaults_to( '' ), #TODO: don't hardcode here - hardcode defaults once in config.py - 'wiki_url' : self._defaults_to( "http://galaxyproject.org/" ), - 'search_url' : self._defaults_to( "http://galaxyproject.org/search/usegalaxy/" ), - 'mailing_lists' : self._defaults_to( "http://wiki.galaxyproject.org/MailingLists" ), - 'screencasts_url' : self._defaults_to( "http://vimeo.com/galaxyproject" ), - 'citation_url' : self._defaults_to( "http://wiki.galaxyproject.org/CitingGalaxy" ), - 'support_url' : self._defaults_to( "http://wiki.galaxyproject.org/Support" ), - 'lims_doc_url' : self._defaults_to( "http://main.g2.bx.psu.edu/u/rkchak/p/sts" ), - 'biostar_url' : lambda t, i, k: i.biostar_url, - 'biostar_url_redirect' : lambda *a: web.url_for( controller='biostar', action='biostar_redirect', - qualified=True ), + 'wiki_url' : _defaults_to( "http://galaxyproject.org/" ), + 'search_url' : _defaults_to( "http://galaxyproject.org/search/usegalaxy/" ), + 'mailing_lists' : _defaults_to( "https://wiki.galaxyproject.org/MailingLists" ), + 'screencasts_url' : _defaults_to( "https://vimeo.com/galaxyproject" ), + 'citation_url' : _defaults_to( "https://wiki.galaxyproject.org/CitingGalaxy" ), + 'support_url' : _defaults_to( "https://wiki.galaxyproject.org/Support" ), + 'lims_doc_url' : _defaults_to( "https://usegalaxy.org/u/rkchak/p/sts" ), + 'biostar_url' : _defaults_to( '' ), + 'biostar_url_redirect' : lambda *a, **c: self.url_for( controller='biostar', action='biostar_redirect', + qualified=True ), - 'allow_user_creation' : lambda t, i, k: i.allow_user_creation, - 'use_remote_user' : lambda t, i, k: i.use_remote_user, - 'remote_user_logout_href' : lambda t, i, k: i.remote_user_logout_href, - 'enable_cloud_launch' : self._defaults_to( False ), - 'datatypes_disable_auto' : self._defaults_to( False ), - 'allow_user_dataset_purge' : self._defaults_to( False ), - 'enable_unique_workflow_defaults' : self._defaults_to( False ), + 'allow_user_creation' : lambda i, k, **c: i.allow_user_creation, + 'use_remote_user' : _defaults_to( None ), + 'remote_user_logout_href' : _defaults_to( '' ), + 'enable_cloud_launch' : _defaults_to( False ), + 'datatypes_disable_auto' : _defaults_to( False ), + 'allow_user_dataset_purge' : _defaults_to( False ), + 'enable_unique_workflow_defaults' : _defaults_to( False ), - 'nginx_upload_path' : self._defaults_to( web.url_for( controller='api', action='tools' ) ), - 'ftp_upload_dir' : self._defaults_to( None ), - 'ftp_upload_site' : self._defaults_to( None ), + 'nginx_upload_path' : _defaults_to( self.url_for( controller='api', action='tools' ) ), + 'ftp_upload_dir' : _defaults_to( None ), + 'ftp_upload_site' : _defaults_to( None ), + 'version_major' : _defaults_to( None ), } class AdminConfigSerializer( ConfigSerializer ): - """Config attributes viewable by admin users""" + # config attributes viewable by admin users def add_serializers( self ): super( AdminConfigSerializer, self ).add_serializers() + def _defaults_to( default ): + return lambda i, k, **c: getattr( i, k, default ) self.serializers.update({ #TODO: this is available from user data, remove 'is_admin_user' : lambda *a: True, - 'library_import_dir' : self._defaults_to( None ), - 'user_library_import_dir' : self._defaults_to( None ), - 'allow_library_path_paste' : self._defaults_to( None ), - 'allow_user_creation' : self._defaults_to( False ), - 'allow_user_deletion' : self._defaults_to( False ), + 'library_import_dir' : _defaults_to( None ), + 'user_library_import_dir' : _defaults_to( None ), + 'allow_library_path_paste' : _defaults_to( False ), + 'allow_user_creation' : _defaults_to( False ), + 'allow_user_deletion' : _defaults_to( False ), }) diff --git a/lib/galaxy/managers/containers.py b/lib/galaxy/managers/containers.py new file mode 100644 index 00000000000..4eae8fe68fd --- /dev/null +++ b/lib/galaxy/managers/containers.py @@ -0,0 +1,151 @@ +""" +Manager mixins to unify the interface into things that can contain: Datasets +and other (nested) containers. + +(e.g. DatasetCollections, Histories, LibraryFolders) + +Histories should be DatasetCollections. +Libraries should be DatasetCollections. +""" + +import operator + +from galaxy import model +import galaxy.exceptions +import galaxy.util + +from galaxy.managers import base + +import logging +log = logging.getLogger( __name__ ) + + +# ==== +class ContainerManagerMixin( object ): + """ + A class that tracks/contains two types of items: + 1) some non-container object (such as datasets) + 2) other sub-containers nested within this one + + Levels of nesting are not considered here; In other words, + each of the methods below only work on the first level of + nesting. + """ + # TODO: this should be an open mapping (not just 2) + #: the classes that can be contained + contained_class = None + subcontainer_class = None + #: how any contents lists produced are ordered + order_contents_on = None + + # ---- interface + def contents( self, container ): + """ + Returns both types of contents: filtered and in some order. + """ + iters = [] + iters.append( self.contained( container ) ) + iters.append( self.subcontainers( container ) ) + return galaxy.util.merge_sorted_iterables( self.order_contents_on, *iters ) + + def contained( self, container, **kwargs ): + """ + Returns non-container objects. + """ + return self._filter_contents( container, self.contained_class, **kwargs ) + + def subcontainers( self, container, **kwargs ): + """ + Returns only the containers within this one. + """ + return self._filter_contents( container, self.subcontainer_class, **kwargs ) + + # ---- private + def _filter_contents( self, container, content_class, **kwargs ): + # TODO: use list (or by_history etc.) + container_filter = self._filter_to_contained( container, content_class ) + query = self.session().query( content_class ).filter( container_filter ) + return query + + def _filter_to_contained( self, container, content_class ): + raise galaxy.exceptions.NotImplemented( 'Abstract class' ) + + def _content_manager( self, content ): + raise galaxy.exceptions.NotImplemented( 'Abstract class' ) + + +class HistoryAsContainerManagerMixin( ContainerManagerMixin ): + + contained_class = model.HistoryDatasetAssociation + subcontainer_class = model.HistoryDatasetCollectionAssociation + order_contents_on = operator.attrgetter( 'hid' ) + + def _filter_to_contained( self, container, content_class ): + return content_class.history == container + + def _content_manager( self, content ): + # type sniffing is inevitable + if isinstance( content, model.HistoryDatasetAssociation ): + return self.hda_manager + elif isinstance( content, model.HistoryDatasetCollectionAssociation ): + return self.hdca_manager + raise TypeError( 'Unknown contents class: ' + str( content ) ) + + +class LibraryFolderAsContainerManagerMixin( ContainerManagerMixin ): + # can contain two types of subcontainer: LibraryFolder, LibraryDatasetCollectionAssociation + # has as the top level container: Library + + contained_class = model.LibraryDataset + subcontainer_class = model.LibraryFolder + # subcontainer_class = model.LibraryDatasetCollectionAssociation + order_contents_on = operator.attrgetter( 'create_time' ) + + def _filter_to_contained( self, container, content_class ): + if content_class == self.subcontainer_class: + return self.subcontainer_class.parent == container + return self.contained_class.folder == container + + def _content_manager( self, content ): + # type snifffing is inevitable + if isinstance( content, model.LibraryDataset ): + return self.lda_manager + elif isinstance( content, model.LibraryFolder ): + return self.folder_manager + raise TypeError( 'Unknown contents class: ' + str( content ) ) + + +class DatasetCollectionAsContainerManagerMixin( ContainerManagerMixin ): + + # (note: unlike the other collections, dc's wrap both contained and subcontainers in this class) + contained_class = model.DatasetCollectionElement + subcontainer_class = model.DatasetCollection + order_contents_on = operator.attrgetter( 'element_index' ) + + def _filter_to_contained( self, container, content_class ): + return content_class.collection == container + + def _content_manager( self, content ): + # type snifffing is inevitable + if isinstance( content, model.DatasetCollectionElement ): + return self.collection_manager + elif isinstance( content, model.DatasetCollection ): + return self.collection_manager + raise TypeError( 'Unknown contents class: ' + str( content ) ) + + +# ==== +class ContainableModelMixin: + """ + Mixin for that which goes in a container. + """ + + # ---- interface + def parent_container( self, containable ): + """ + Return this item's parent container or None if unrecorded. + """ + raise galaxy.exceptions.NotImplemented( 'Abstract class' ) + + def set_parent_container( self, containable, new_parent_container ): + raise galaxy.exceptions.NotImplemented( 'Abstract class' ) diff --git a/lib/galaxy/managers/datasets.py b/lib/galaxy/managers/datasets.py index 802845d8063..324baa2bc67 100644 --- a/lib/galaxy/managers/datasets.py +++ b/lib/galaxy/managers/datasets.py @@ -3,10 +3,12 @@ Manager and Serializer for Datasets. """ from galaxy import model from galaxy import exceptions +import galaxy.datatypes.metadata from galaxy.managers import base from galaxy.managers import secured from galaxy.managers import deletable +from galaxy.managers import rbac_secured from galaxy.managers import users import logging @@ -24,184 +26,132 @@ class DatasetManager( base.ModelManager, secured.AccessibleManagerMixin, deletab def __init__( self, app ): super( DatasetManager, self ).__init__( app ) + self.permissions = DatasetRBACPermissions( app ) # need for admin test self.user_manager = users.UserManager( app ) - def create( self, trans, flush=True, **kwargs ): + def create( self, manage_roles=None, access_roles=None, flush=True, **kwargs ): """ Create and return a new Dataset object. """ # default to NEW state on new datasets kwargs.update( dict( state=( kwargs.get( 'state', model.Dataset.states.NEW ) ) ) ) dataset = model.Dataset( **kwargs ) + self.session().add( dataset ) + + self.permissions.set( dataset, manage_roles, access_roles, flush=False ) - self.app.model.context.add( dataset ) if flush: - self.app.model.context.flush() + self.session().flush() return dataset - # def copy( self, dataset, **kwargs ): - # """ - # Clone, update, and return the given dataset. - # """ - # pass + def copy( self, dataset, **kwargs ): + raise galaxy.exceptions.NotImplemented( 'Datasets cannot be copied' ) - # def to_hda( self, trans, dataset, history, **kwargs ): - # """ - # Create an hda from this dataset. - # """ - # pass - - # def to_ldda( self, trans, dataset, library_folder, **kwargs ): - # """ - # Create an ldda from this dataset. - # """ - # pass - - # TODO: this may be more conv. somewhere else - # TODO: how to allow admin bypass? - def error_unless_dataset_purge_allowed( self, trans, item, msg=None ): - if not self.app.config.allow_user_dataset_purge: - msg = msg or 'This instance does not allow user dataset purging' - raise exceptions.ConfigDoesNotAllowException( msg ) - return item - - def purge( self, trans, dataset, flush=True ): + def purge( self, dataset, flush=True ): """ Remove the object_store/file for this dataset from storage and mark as purged. :raises exceptions.ConfigDoesNotAllowException: if the instance doesn't allow """ - self.error_unless_dataset_purge_allowed( trans, dataset ) + self.error_unless_dataset_purge_allowed( dataset ) # the following also marks dataset as purged and deleted dataset.full_delete() - self.app.model.context.add( dataset ) + self.session().add( dataset ) if flush: - self.app.model.context.flush() + self.session().flush() return dataset + # TODO: this may be more conv. somewhere else + # TODO: how to allow admin bypass? + def error_unless_dataset_purge_allowed( self, msg=None ): + if not self.app.config.allow_user_dataset_purge: + msg = msg or 'This instance does not allow user dataset purging' + raise exceptions.ConfigDoesNotAllowException( msg ) + # .... accessibility # datasets can implement the accessible interface, but accessibility is checked in an entirely different way # than those resources that have a user attribute (histories, pages, etc.) - def is_accessible( self, trans, dataset, user ): + def is_accessible( self, dataset, user, **kwargs ): """ Is this dataset readable/viewable to user? """ - if self.user_manager.is_admin( trans, user ): + if self.user_manager.is_admin( user ): return True - if self.has_access_permission( trans, dataset, user ): + if self.has_access_permission( dataset, user ): return True return False - def has_access_permission( self, trans, dataset, user ): + def has_access_permission( self, dataset, user ): """ Return T/F if the user has role-based access to the dataset. """ roles = user.all_roles() if user else [] return self.app.security_agent.can_access_dataset( roles, dataset ) - # TODO: these need work - def _access_permission( self, trans, dataset, user=None, role=None ): - """ - Return most recent DatasetPermissions for the dataset and user. - """ - security_agent = self.app.security_agent - access_action = security_agent.permitted_actions.DATASET_ACCESS.action - - # get a list of role ids to check access for (defaulting to all_roles) - user_roles = [ role ] if role else user.all_roles() - user_role_ids = [ r.id for r in user_roles ] - query = ( self.app.model.context.query( model.DatasetPermissions ) - .filter( model.DatasetPermissions.action == access_action ) - .filter( model.DatasetPermissions.dataset == dataset ) - .filter( model.DatasetPermissions.role_id.in_( user_role_ids ) ) ) - # TODO:?? most recent? - return query.first() - - def _create_access_permission( self, trans, dataset, role, flush=True ): - """ - Create a new DatasetPermissions for the given dataset and role. - """ - access_action = self.app.security_agent.permitted_actions.DATASET_ACCESS.action - permission = model.DatasetPermissions( access_action, dataset, role ) - self.app.model.context.add( permission ) - if flush: - self.app.model.context.flush() - return permission - - # def give_access_permission( self, trans, dataset, user, flush=True ): - # """ - # """ - # # for now, use the user's private role - # security_agent = self.app.security_agent - # user_role = security_agent.get_private_user_role( user ) - # - # existing_permission = self._access_permission( trans, dataset, role=user_role ) - # print 'existing access_perm:', existing_permission - # if existing_permission: - # return dataset - # - # permission = self._create_access_permission( trans, dataset, user_role ) - # print 'access_roles:', [ role.name for role in dataset.get_access_roles( trans ) ] - # - # #access_action = security_agent.permitted_actions.DATASET_ACCESS.action - # #access_action = security_agent.get_action( access_action ) - # #permissions = { access_action : [ user_role ] } - # #security_agent.set_dataset_permission( dataset, permissions ) - # - # #self.app.model.context.add( dataset ) - # #if flush: - # # self.app.model.context.flush() - # - # dbl_chk = self._access_permission( trans, dataset, role=user_role ) - # print 'access_perm:', dbl_chk - # - # return dataset - - # def remove_access_permission( self, trans, dataset, user ): - # """ - # """ - # pass - - # .... manage/modify - # def has_manage_permission( self, trans, dataset, user ): - # """ - # """ - # pass - # - # def give_manage_permission( self, trans, dataset, user ): - # """ - # """ - # pass - # - # def remove_manage_permission( self, trans, dataset, user ): - # """ - # """ - # pass - # TODO: implement above for groups # TODO: datatypes? - # .... data, object_store +# TODO: SecurityAgentDatasetRBACPermissions( object ): + +class DatasetRBACPermissions( object ): + + def __init__( self, app ): + self.app = app + self.access = rbac_secured.AccessDatasetRBACPermission( app ) + self.manage = rbac_secured.ManageDatasetRBACPermission( app ) + + # TODO: temporary facade over security_agent + def available_roles( self, trans, dataset, controller='root' ): + return self.app.security_agent.get_legitimate_roles( trans, dataset, controller ) + + # TODO: not enough by a long shot + def get( self, dataset, flush=True ): + manage = self.manage.by_dataset( dataset ) + access = self.access.by_dataset( dataset ) + return ( manage, access ) + + def set( self, dataset, manage_roles, access_roles, flush=True ): + manage = self.manage.set( dataset, manage_roles or [], flush=False ) + access = self.access.set( dataset, access_roles or [], flush=flush ) + return ( manage, access ) + + # ---- conv. settings + def set_public_with_single_manager( self, dataset, user, flush=True ): + manage = self.manage.grant( dataset, user, flush=flush ) + self.access.clear( dataset, flush=False ) + return ( [ manage ], [] ) + + def set_private_to_one_user( self, dataset, user, flush=True ): + manage = self.manage.grant( dataset, user, flush=False ) + access = self.access.set_private( dataset, user, flush=flush ) + return ( [ manage ], access ) + + class DatasetSerializer( base.ModelSerializer, deletable.PurgableSerializerMixin ): def __init__( self, app ): super( DatasetSerializer, self ).__init__( app ) + self.dataset_manager = DatasetManager( app ) self.default_view = 'summary' self.add_view( 'summary', [ 'id', - 'create_time', 'update_time', + 'create_time', + 'update_time', 'state', - 'deleted', 'purged', 'purgable', + 'deleted', + 'purged', + 'purgable', # 'object_store_id', # 'external_filename', # 'extra_files_path', - 'file_size', 'total_size', + 'file_size', + 'total_size', 'uuid', ]) # could do visualizations and/or display_apps @@ -211,23 +161,73 @@ class DatasetSerializer( base.ModelSerializer, deletable.PurgableSerializerMixin deletable.PurgableSerializerMixin.add_serializers( self ) self.serializers.update({ - 'id': self.serialize_id, - 'create_time': self.serialize_date, - 'update_time': self.serialize_date, - 'uuid': lambda t, i, k: str( i.uuid ) if i.uuid else None, + 'create_time' : self.serialize_date, + 'update_time' : self.serialize_date, + + 'uuid' : lambda i, k, **c: str( i.uuid ) if i.uuid else None, + 'file_name' : self.serialize_file_name, + 'extra_files_path' : self.serialize_extra_files_path, + 'permissions' : self.serialize_permissions, + + 'total_size' : lambda i, k, **c: int( i.get_total_size() ), + 'file_size' : lambda i, k, **c: int( i.get_size() ) }) + def serialize_file_name( self, dataset, key, **context ): + """ + If the config allows or the user is admin, return the file name + of the file that contains this dataset's data. + """ + # TODO: allow admin + # conifg due to cost of operation + if not self.app.config.expose_dataset_path: + self.skip() + return dataset.file_name + + def serialize_extra_files_path( self, dataset, key, **context ): + """ + If the config allows or the user is admin, return the file path. + """ + # TODO: allow admin + # conifg due to cost of operation + if not self.app.config.expose_dataset_path: + self.skip() + return dataset.extra_files_path + + def serialize_permissions( self, dataset, key, **context ): + """ + """ + permissions = {} +# TODO: use rbac permissions? + return permissions + class DatasetDeserializer( base.ModelDeserializer, deletable.PurgableDeserializerMixin ): model_manager_class = DatasetManager def add_deserializers( self ): super( DatasetDeserializer, self ).add_deserializers() + # not much to set here besides permissions and purged/deleted deletable.PurgableDeserializerMixin.add_deserializers( self ) + self.deserializers.update({ + 'permissions' : self.deserialize_permissions, + }) + + def deserialize_permissions( self, dataset, key, value, **context ): + """ + """ + permissions = {} +# TODO: test if different - it's an expensive op +# TODO: validation will be tricky +# TODO: use rbac permissions? + return permissions + # ============================================================================= AKA DatasetInstanceManager -class DatasetAssociationManager( base.ModelManager, secured.AccessibleManagerMixin, deletable.PurgableManagerMixin ): +class DatasetAssociationManager( base.ModelManager, + secured.AccessibleManagerMixin, + deletable.PurgableManagerMixin ): """ DatasetAssociation/DatasetInstances are intended to be working proxies to a Dataset, associated with either a library or a @@ -241,32 +241,227 @@ class DatasetAssociationManager( base.ModelManager, secured.AccessibleManagerMix super( DatasetAssociationManager, self ).__init__( app ) self.dataset_manager = DatasetManager( app ) - def is_accessible( self, trans, dataset_assoc, user ): + def is_accessible( self, dataset_assoc, user, **kwargs ): """ Is this DA accessible to `user`? """ # defer to the dataset - return self.dataset_manager.is_accessible( trans, dataset_assoc.dataset, user ) + return self.dataset_manager.is_accessible( dataset_assoc.dataset, user ) - # def metadata( self, trans, dataset_assoc ): - # """ - # Return the metadata collection. - # """ - # # get metadata - # pass + def purge( self, dataset_assoc, flush=True ): + """ + Purge this DatasetInstance and the dataset underlying it. + """ + # error here if disallowed - before jobs are stopped + # TODO: this check may belong in the controller + self.dataset_manager.error_unless_dataset_purge_allowed() + super( DatasetAssociationManager, self ).purge( dataset_assoc, flush=flush ) - # def is_being_used( self, trans, dataset_assoc ): - # """ - # """ - # #TODO: check history_associations, library_associations - # pass + # stop any jobs outputing the dataset_assoc + if dataset_assoc.creating_job_associations: + job = dataset_assoc.creating_job_associations[0].job + if not job.finished: + # signal to stop the creating job + job.mark_deleted( self.app.config.track_jobs_in_database ) + self.app.job_manager.job_stop_queue.put( job.id ) + + # more importantly, purge underlying dataset as well + if dataset_assoc.dataset.user_can_purge: + self.dataset_manager.purge( dataset_assoc.dataset ) + return dataset_assoc + + def by_user( self, user ): + """ + """ + raise galaxy.exceptions.NotImplemented( 'Abstract Method' ) + + # .... associated job + def creating_job( self, dataset_assoc ): + # TODO: is this needed? Can't you use the dataset_assoc.creating_job attribute? When is this None? + # TODO: this would be even better if outputs and inputs were the underlying datasets + job = None + for job_output_assoc in dataset_assoc.creating_job_associations: + job = job_output_assoc.job + break + return job + + def stop_creating_job( self, dataset_assoc ): + """ + Stops an dataset_assoc's creating job if all the job's other outputs are deleted. + """ + # TODO: use in purge above + RUNNING_STATES = ( + self.app.model.Job.states.QUEUED, + self.app.model.Job.states.RUNNING, + self.app.model.Job.states.NEW + ) + if dataset_assoc.parent_id is None and len( dataset_assoc.creating_job_associations ) > 0: + # Mark associated job for deletion + job = dataset_assoc.creating_job_associations[0].job + if job.state in RUNNING_STATES: + # Are *all* of the job's other output datasets deleted? + if job.check_if_output_datasets_deleted(): + job.mark_deleted( self.app.config.track_jobs_in_database ) + self.app.job_manager.job_stop_queue.put( job.id ) + return True + return False -class DatasetAssociationSerializer( base.ModelSerializer, deletable.PurgableSerializerMixin ): +class _UnflattenedMetadataDatasetAssociationSerializer( base.ModelSerializer, + deletable.PurgableSerializerMixin ): + + def __init__( self, app ): + self.dataset_serializer = DatasetSerializer( app ) + super( _UnflattenedMetadataDatasetAssociationSerializer, self ).__init__( app ) + + def add_serializers( self ): + super( _UnflattenedMetadataDatasetAssociationSerializer, self ).add_serializers() + deletable.PurgableSerializerMixin.add_serializers( self ) + + self.serializers.update({ + 'create_time' : self.serialize_date, + 'update_time' : self.serialize_date, + + # underlying dataset + 'dataset' : lambda i, k, **c: self.dataset_serializer.serialize_to_view( i.dataset, view='summary', **c ), + 'dataset_id' : self._proxy_to_dataset( key='id' ), + # TODO: why is this named uuid!? The da doesn't have a uuid - it's the underlying dataset's uuid! + 'uuid' : self._proxy_to_dataset( key='uuid' ), + # 'dataset_uuid' : self._proxy_to_dataset( key='uuid' ), + 'file_name' : self._proxy_to_dataset( serializer=self.dataset_serializer.serialize_file_name ), + 'extra_files_path' : self._proxy_to_dataset( serializer=self.dataset_serializer.serialize_extra_files_path ), + 'permissions' : self._proxy_to_dataset( serializer=self.dataset_serializer.serialize_permissions), + # TODO: do the sizes proxy accurately/in the same way? + 'size' : lambda i, k, **c: int( i.get_size() ), + 'file_size' : lambda i, k, **c: self.serializers[ 'size' ]( i, k, **c ), + 'nice_size' : lambda i, k, **c: i.get_size( nice_size=True ), + + # common to lddas and hdas - from mapping.py + 'copied_from_history_dataset_association_id' : self.serialize_id, + 'copied_from_library_dataset_dataset_association_id': self.serialize_id, + 'info' : lambda i, k, **c: i.info.strip() if isinstance( i.info, basestring ) else i.info, + 'blurb' : lambda i, k, **c: i.blurb, + 'peek' : lambda i, k, **c: i.display_peek() if i.peek and i.peek != 'no peek' else None, + + 'meta_files' : self.serialize_meta_files, + 'metadata' : self.serialize_metadata, + + 'parent_id' : self.serialize_id, + 'designation' : lambda i, k, **c: i.designation, + + # 'extended_metadata' : self.serialize_extended_metadata, + # 'extended_metadata_id' : self.serialize_id, + + # remapped + 'genome_build' : lambda i, k, **c: i.dbkey, + + # derived (not mapped) attributes + 'data_type' : lambda i, k, **c: i.datatype.__class__.__module__ + '.' + i.datatype.__class__.__name__, + + # TODO: conversions + # TODO: metadata/extra files + }) + # this an abstract superclass, so no views created + # because of that: we need to add a few keys that will use the default serializer + self.serializable_keyset.update([ 'name', 'state', 'tool_version', 'extension', 'visible', 'dbkey' ]) + + def _proxy_to_dataset( self, serializer=None, key=None ): + if key: + serializer = self.dataset_serializer.serializers.get( key ) + if serializer: + return lambda i, k, **c: serializer( i.dataset, key or k, **c ) + raise TypeError( 'kwarg serializer or key needed') + + def serialize_meta_files( self, dataset_assoc, key, **context ): + """ + Cycle through meta files and return them as a list of dictionaries. + """ + meta_files = [] + for meta_type in dataset_assoc.metadata.spec.keys(): + if isinstance( dataset_assoc.metadata.spec[ meta_type ].param, galaxy.datatypes.metadata.FileParameter ): + meta_files.append( dict( file_type=meta_type ) ) + return meta_files + + def serialize_metadata( self, dataset_assoc, key, excluded=None, **context ): + """ + Cycle through metadata and return as dictionary. + """ + # dbkey is a repeat actually (metadata_dbkey == genome_build) + # excluded = [ 'dbkey' ] if excluded is None else excluded + excluded = [] if excluded is None else excluded + + metadata = {} + for name, spec in dataset_assoc.metadata.spec.items(): + if name in excluded: + continue + val = dataset_assoc.metadata.get( name ) + # NOTE: no files + if isinstance( val, model.MetadataFile ): + # only when explicitly set: fetching filepaths can be expensive + if not self.app.config.expose_dataset_path: + continue + val = val.file_name + # TODO:? possibly split this off? + # If no value for metadata, look in datatype for metadata. + elif val is None and hasattr( dataset_assoc.datatype, name ): + val = getattr( dataset_assoc.datatype, name ) + metadata[ name ] = val + + return metadata + + +class DatasetAssociationSerializer( _UnflattenedMetadataDatasetAssociationSerializer ): + # TODO: remove this class - metadata should be a sub-object instead as in the superclass def add_serializers( self ): super( DatasetAssociationSerializer, self ).add_serializers() - deletable.PurgableSerializerMixin.add_serializers( self ) + # remove the single nesting key here + del self.serializers[ 'metadata' ] + + def serialize( self, dataset_assoc, keys, **context ): + """ + Override to add metadata as flattened keys on the serialized DatasetInstance. + """ + # if 'metadata' isn't removed from keys here serialize will retrieve the un-serializable MetadataCollection + # TODO: remove these when metadata is sub-object + KEYS_HANDLED_SEPARATELY = ( 'metadata', ) + left_to_handle = self._pluck_from_list( keys, KEYS_HANDLED_SEPARATELY ) + serialized = super( DatasetAssociationSerializer, self ).serialize( dataset_assoc, keys, **context ) + + # add metadata directly to the dict instead of as a sub-object + if 'metadata' in left_to_handle: + metadata = self._prefixed_metadata( dataset_assoc ) + serialized.update( metadata ) + return serialized + + # TODO: this is more util/gen. use + def _pluck_from_list( self, l, elems ): + """ + Removes found elems from list l and returns list of found elems if found. + """ + found = [] + for elem in elems: + try: + index = l.index( elem ) + found.append( l.pop( index ) ) + except ValueError: + pass + return found + + def _prefixed_metadata( self, dataset_assoc ): + """ + Adds (a prefixed version of) the DatasetInstance metadata to the dict, + prefixing each key with 'metadata_'. + """ + # build the original, nested dictionary + metadata = self.serialize_metadata( dataset_assoc, 'metadata' ) + + # prefix each key within and return + prefixed = {} + for key, val in metadata.items(): + prefixed_key = 'metadata_' + key + prefixed[ prefixed_key ] = val + return prefixed class DatasetAssociationDeserializer( base.ModelDeserializer, deletable.PurgableDeserializerMixin ): @@ -275,15 +470,75 @@ class DatasetAssociationDeserializer( base.ModelDeserializer, deletable.Purgable super( DatasetAssociationDeserializer, self ).add_deserializers() deletable.PurgableDeserializerMixin.add_deserializers( self ) + self.deserializers.update({ + 'name' : self.deserialize_basestring, + 'info' : self.deserialize_basestring, + }) + self.deserializable_keyset.update( self.deserializers.keys() ) -class DatasetAssociationFilters( base.ModelFilterParser, deletable.PurgableFiltersMixin ): +# TODO: untested + def deserialize_metadata( self, dataset_assoc, metadata_key, metadata_dict, **context ): + """ + """ + self.validate.type( metadata_key, metadata_dict, dict ) + returned = {} + for key, val in metadata_dict.items(): + returned[ key ] = self.deserialize_metadatum( dataset_assoc, key, val, **context ) + return returned + + def deserialize_metadatum( self, dataset_assoc, key, val, **context ): + """ + """ + if key not in dataset_assoc.datatype.metadata_spec: + return + metadata_specification = dataset_assoc.datatype.metadata_spec[ key ] + if metadata_specification.get( 'readonly' ): + return + unwrapped_val = metadata_specification.unwrap( val ) + setattr( dataset_assoc.metadata, key, unwrapped_val ) + # ...? + return unwrapped_val + + +class DatasetAssociationFilterParser( base.ModelFilterParser, deletable.PurgableFiltersMixin ): def _add_parsers( self ): - super( DatasetAssociationFilters, self )._add_parsers() + super( DatasetAssociationFilterParser, self )._add_parsers() deletable.PurgableFiltersMixin._add_parsers( self ) self.orm_filter_parsers.update({ - 'name': { 'op': ( 'eq', 'contains', 'like' ) }, - 'state': { 'op': ( 'eq', 'in' ) }, + 'name' : { 'op': ( 'eq', 'contains', 'like' ) }, + 'state' : { 'column' : '_state', 'op': ( 'eq', 'in' ) }, + 'visible' : { 'op': ( 'eq' ), 'val': self.parse_bool }, }) - # self.fn_filter_parsers.update({}) + self.fn_filter_parsers.update({ + 'genome_build' : self.string_standard_ops( 'dbkey' ), + 'data_type' : { + 'op': { + 'eq' : self.eq_datatype, + 'isinstance' : self.isinstance_datatype + } + } + }) + + def eq_datatype( self, dataset_assoc, class_str ): + """ + Is the `dataset_assoc` datatype equal to the registered datatype `class_str`? + """ + comparison_class = self.app.datatypes_registry.get_datatype_class_by_name( class_str ) + return ( comparison_class + and dataset_assoc.datatype.__class__ == comparison_class ) + + def isinstance_datatype( self, dataset_assoc, class_strs ): + """ + Is the `dataset_assoc` datatype derived from any of the registered + datatypes in the comma separated string `class_strs`? + """ + parse_datatype_fn = self.app.datatypes_registry.get_datatype_class_by_name + comparison_classes = [] + for class_str in class_strs.split( ',' ): + datatype_class = parse_datatype_fn( class_str ) + if datatype_class: + comparison_classes.append( datatype_class ) + return ( comparison_classes + and isinstance( dataset_assoc.datatype, comparison_classes ) ) diff --git a/lib/galaxy/managers/deletable.py b/lib/galaxy/managers/deletable.py index b8090e84016..dc55eea5baf 100644 --- a/lib/galaxy/managers/deletable.py +++ b/lib/galaxy/managers/deletable.py @@ -18,14 +18,13 @@ class DeletableManagerMixin( object ): that they are no longer needed, should not be displayed, or may be actually removed by an admin/script. """ - - def delete( self, trans, item, flush=True, **kwargs ): + def delete( self, item, flush=True, **kwargs ): """ Mark as deleted and return. """ return self._session_setattr( item, 'deleted', True, flush=flush ) - def undelete( self, trans, item, flush=True, **kwargs ): + def undelete( self, item, flush=True, **kwargs ): """ Mark as not deleted and return. """ @@ -44,7 +43,7 @@ class DeletableDeserializerMixin( object ): def add_deserializers( self ): self.deserializers[ 'deleted' ] = self.deserialize_deleted - def deserialize_deleted( self, trans, item, key, val ): + def deserialize_deleted( self, item, key, val, **context ): """ Delete or undelete `item` based on `val` then return `item.deleted`. """ @@ -53,9 +52,9 @@ class DeletableDeserializerMixin( object ): return item.deleted # TODO:?? flush=False? if new_deleted: - self.manager.delete( trans, item, flush=False ) + self.manager.delete( item, flush=False ) else: - self.manager.undelete( trans, item, flush=False ) + self.manager.undelete( item, flush=False ) return item.deleted @@ -73,8 +72,7 @@ class PurgableManagerMixin( DeletableManagerMixin ): purging is often removal of some additional, non-db resource (e.g. a dataset's file). """ - - def purge( self, trans, item, flush=True, **kwargs ): + def purge( self, item, flush=True, **kwargs ): """ Mark as purged and return. @@ -96,15 +94,16 @@ class PurgableDeserializerMixin( DeletableDeserializerMixin ): DeletableDeserializerMixin.add_deserializers( self ) self.deserializers[ 'purged' ] = self.deserialize_purged - def deserialize_purged( self, trans, item, key, val ): + def deserialize_purged( self, item, key, val, **context ): """ If `val` is True, purge `item` and return `item.purged`. """ new_purged = self.validate.bool( key, val ) if new_purged == item.purged: return item.purged + # do we want to error if something attempts to 'unpurge'? if new_purged: - self.manager.purge( trans, item, flush=False ) + self.manager.purge( item, flush=False ) return item.purged diff --git a/lib/galaxy/managers/hdas.py b/lib/galaxy/managers/hdas.py index e99010feac6..65a6665fdc1 100644 --- a/lib/galaxy/managers/hdas.py +++ b/lib/galaxy/managers/hdas.py @@ -11,7 +11,6 @@ import gettext from galaxy import model from galaxy import exceptions from galaxy import datatypes -import galaxy.datatypes.metadata from galaxy import objectstore from galaxy.managers import datasets @@ -48,28 +47,35 @@ class HDAManager( datasets.DatasetAssociationManager, self.user_manager = users.UserManager( app ) # .... security and permissions - def is_accessible( self, trans, hda, user ): + def is_accessible( self, hda, user, **kwargs ): """ Override to allow owners (those that own the associated history). """ - if self.is_owner( trans, hda, user ): - return True - return super( HDAManager, self ).is_accessible( trans, hda, user ) + # this, apparently, is not True: + # if I have a copy of a dataset and anyone who manages permissions on it revokes my access + # I can not access that dataset even if it's in my history + # if self.is_owner( hda, user, **kwargs ): + # return True + return super( HDAManager, self ).is_accessible( hda, user ) - def is_owner( self, trans, hda, user ): + def is_owner( self, hda, user, current_history=None, **kwargs ): """ Use history to see if current user owns HDA. """ history = hda.history + if self.user_manager.is_admin( user ): + return True + # allow anonymous user to access current history # TODO: some dup here with historyManager.is_owner but prevents circ import - if self.user_manager.is_admin( trans, user ): - return True - if self.user_manager.is_anonymous( user ) and history == trans.get_history(): - return True + # TODO: awkward kwarg (which is my new band name); this may not belong here - move to controller? + if self.user_manager.is_anonymous( user ): + if current_history and history == current_history: + return True + return False return history.user == user # .... create and copy - def create( self, trans, history=None, dataset=None, flush=True, **kwargs ): + def create( self, history=None, dataset=None, flush=True, **kwargs ): """ Create a new hda optionally passing in it's history and dataset. @@ -78,25 +84,19 @@ class HDAManager( datasets.DatasetAssociationManager, """ if not dataset: kwargs[ 'create_dataset' ] = True - hda = super( HDAManager, self ).create( trans, - flush=flush, - history=history, - dataset=dataset, - sa_session=self.app.model.context, **kwargs ) + hda = model.HistoryDatasetAssociation( history=history, dataset=dataset, + sa_session=self.app.model.context, **kwargs ) if history: - # TODO Probably Bug: set_hid is never used, and should be passed - # to history.add_dataset here. - set_hid = not ( 'hid' in kwargs ) - history.add_dataset( hda ) + history.add_dataset( hda, set_hid=( 'hid' not in kwargs ) ) #TODO:?? some internal sanity check here (or maybe in add_dataset) to make sure hids are not duped? - self.app.model.context.add( hda ) + self.session().add( hda ) if flush: - self.app.model.context.flush() + self.session().flush() return hda - def copy( self, trans, hda, history=None, **kwargs ): + def copy( self, hda, history=None, **kwargs ): """ Copy and return the given HDA. """ @@ -125,58 +125,36 @@ class HDAManager( datasets.DatasetAssociationManager, # TODO: update from kwargs? # Need to set after flushed, as MetadataFiles require dataset.id - self.app.model.context.add( copy ) - self.app.model.context.flush() + self.session().add( copy ) + self.session().flush() copy.metadata = hda.metadata # In some instances peek relies on dataset_id, i.e. gmaj.zip for viewing MAFs if not hda.datatype.copy_safe_peek: copy.set_peek() - self.app.model.context.flush() + self.session().flush() return copy - def copy_ldda( self, trans, history, ldda, **kwargs ): + def copy_ldda( self, history, ldda, **kwargs ): """ Copy this HDA as a LDDA and return. """ return ldda.to_history_dataset_association( history, add_to_history=True ) - # TODO: stub - # def is_a_copy( self, trans, hda ): - # pass - - # TODO: stub - # def copied_from( self, trans, hda ): - # pass - - # TODO: stub - # def by_user( self, trans, user ): - # pass - - def purge( self, trans, hda, flush=True ): + # .... deletion and purging + def purge( self, hda, current_user=None, flush=True ): """ Purge this HDA and the dataset underlying it. """ - # error here if disallowed - before jobs are stopped - # TODO: poss. move to DatasetAssociationManager - self.dataset_manager.error_unless_dataset_purge_allowed( trans, hda ) - if trans.user: - trans.user.total_disk_usage -= hda.quota_amount( trans.user ) - super( HDAManager, self ).purge( trans, hda, flush=flush ) - if hda.creating_job_associations: - job = hda.creating_job_associations[0].job - if not job.finished: - # signal to stop the creating job - job.mark_deleted( self.app.config.track_jobs_in_database ) - self.app.job_manager.job_stop_queue.put( job.id ) - - # more importantly, purge dataset as well - if hda.dataset.user_can_purge: - self.dataset_manager.purge( trans, hda.dataset ) + super( HDAManager, self ).purge( hda, flush=flush ) + # decrease the user's space used + if current_user: + current_user.total_disk_usage -= hda.quota_amount( current_user ) return hda - def error_if_uploading( self, trans, hda ): + # .... states + def error_if_uploading( self, hda ): """ Raise error if HDA is still uploading. """ @@ -185,23 +163,14 @@ class HDAManager( datasets.DatasetAssociationManager, raise exceptions.Conflict( "Please wait until this dataset finishes uploading" ) return hda - # .... via history - # def by_history_id( self, trans, history_id, filters=None, **kwargs ): - # history_id_filter = self.model_class.history_id == history_id - # filters = self._munge_filters( history_id_filter, filters ) - # return self.list( trans, filters=filters, **kwargs ) - - # def by_history( self, trans, history, filters=None, **kwargs ): - # return history.datasets - - # .... associated - def creating_job( self, trans, hda ): - # TODO: is this needed? Can't you use the hda.creating_job attribute? When is this None? - job = None - for job_output_assoc in hda.creating_job_associations: - job = job_output_assoc.job - break - return job + def has_been_resubmitted( self, hda ): + """ + Return True if the hda's job was resubmitted at any point. + """ + job_states = model.Job.states + query = ( self._job_state_history_query( hda ) + .filter( model.JobStateHistory.state == job_states.RESUBMITTED ) ) + return self.app.model.context.query( query.exists() ).scalar() def _job_state_history_query( self, hda ): """ @@ -220,130 +189,43 @@ class HDAManager( datasets.DatasetAssociationManager, .enable_eagerloads( False ) ) return query - def has_been_resubmitted( self, hda ): + def data_conversion_status( self, hda ): """ - Return True if the hda's job was resubmitted at any point. + Returns a message if an hda is not ready to be used in visualization. """ - job_states = model.Job.states - query = ( self._job_state_history_query( hda ) - .filter( model.JobStateHistory.state == job_states.RESUBMITTED ) ) - return self.app.model.context.query( query.exists() ).scalar() - - # .... serialization - def get_display_apps( self, trans, hda ): - """ - Return dictionary containing new-style display app urls. - """ - display_apps = [] - for display_app in hda.get_display_applications( trans ).itervalues(): - - app_links = [] - for link_app in display_app.links.itervalues(): - app_links.append({ - 'target': link_app.url.get( 'target_frame', '_blank' ), - 'href': link_app.get_display_url( hda, trans ), - 'text': gettext.gettext( link_app.name ) - }) - if app_links: - display_apps.append( dict( label=display_app.name, links=app_links ) ) - - return display_apps - - def get_old_display_applications( self, trans, hda ): - """ - Return dictionary containing old-style display app urls. - """ - display_apps = [] - if not self.app.config.enable_old_display_applications: - return display_apps - - for display_app in hda.datatype.get_display_types(): - target_frame, display_links = hda.datatype.get_display_links( hda, - display_app, - self.app, - trans.request.base ) - - if len( display_links ) > 0: - display_label = hda.datatype.get_display_label( display_app ) - - app_links = [] - for display_name, display_link in display_links: - app_links.append({ - 'target': target_frame, - 'href': display_link, - 'text': gettext.gettext( display_name ) - }) - if app_links: - display_apps.append( dict( label=display_label, links=app_links ) ) - - return display_apps - - def get_visualizations( self, trans, hda ): - """ - Return a list of dictionaries with links to visualization pages - for those visualizations that apply to this hda. - """ - # use older system if registry is off in the config - if not self.app.visualizations_registry: - return hda.get_visualizations() - return self.app.visualizations_registry.get_visualizations( trans, hda ) + # this is a weird syntax and return val + if not hda: + return self.model_class.conversion_messages.NO_DATA + if hda.state == model.Job.states.ERROR: + return self.model_class.conversion_messages.ERROR + if hda.state != model.Job.states.OK: + return self.model_class.conversion_messages.PENDING + return None + # .... data # TODO: to data provider or Text datatype directly - def text_data( self, dataset, preview=True ): + def text_data( self, hda, preview=True ): """ Get data from text file, truncating if necessary. """ + # 1 MB + MAX_PEEK_SIZE = 1000000 + truncated = False - dataset_data = None - if os.path.exists( dataset.file_name ): - if isinstance( dataset.datatype, datatypes.data.Text ): - max_peek_size = 1000000 # 1 MB - if preview and os.stat( dataset.file_name ).st_size > max_peek_size: - dataset_data = open( dataset.file_name ).read( max_peek_size ) - truncated = True - else: - dataset_data = open( dataset.file_name ).read( max_peek_size ) - truncated = False - else: - # For now, cannot get data from non-text datasets. - dataset_data = None - return truncated, dataset_data + hda_data = None + # For now, cannot get data from non-text datasets. + if not isinstance( hda.datatype, datatypes.data.Text ): + return truncated, hda_data + if not os.path.exists( hda.file_name ): + return truncated, hda_data - # this is a weird syntax and return val - def data_conversion_status( self, trans, hda ): - """ - Returns a message if dataset is not ready to be used in visualization. - """ - if not hda: - return hda.conversion_messages.NO_DATA - if hda.state == model.Job.states.ERROR: - return hda.conversion_messages.ERROR - if hda.state != model.Job.states.OK: - return hda.conversion_messages.PENDING - return None - - def stop_creating_job( self, hda ): - """ - Stops an HDA's creating job if all the job's other outputs are deleted. - """ - RUNNING_STATES = ( - self.app.model.Job.states.QUEUED, - self.app.model.Job.states.RUNNING, - self.app.model.Job.states.NEW - ) - if hda.parent_id is None and len( hda.creating_job_associations ) > 0: - # Mark associated job for deletion - job = hda.creating_job_associations[0].job - if job.state in RUNNING_STATES: - # Are *all* of the job's other output datasets deleted? - if job.check_if_output_datasets_deleted(): - job.mark_deleted( self.app.config.track_jobs_in_database ) - self.app.job_manager.job_stop_queue.put( job.id ) - return True - return False + truncated = preview and os.stat( hda.file_name ).st_size > MAX_PEEK_SIZE + hda_data = open( hda.file_name ).read( MAX_PEEK_SIZE ) + return truncated, hda_data -class HDASerializer( datasets.DatasetAssociationSerializer, +class HDASerializer( # datasets._UnflattenedMetadataDatasetAssociationSerializer, + datasets.DatasetAssociationSerializer, taggable.TaggableSerializerMixin, annotatable.AnnotatableSerializerMixin ): # TODO: inherit from datasets.DatasetAssociationSerializer @@ -356,6 +238,7 @@ class HDASerializer( datasets.DatasetAssociationSerializer, self.default_view = 'summary' self.add_view( 'summary', [ 'id', 'name', + 'type_id', 'history_id', 'hid', # why include if model_class is there? 'history_content_type', @@ -375,18 +258,16 @@ class HDASerializer( datasets.DatasetAssociationSerializer, # remapped 'genome_build', 'misc_info', 'misc_blurb', 'file_ext', 'file_size', + 'file_path', 'create_time', 'update_time', 'resubmitted', 'metadata', 'meta_files', 'data_type', 'peek', - #TODO: why is this named uuid!? The hda doesn't have a uuid - it's the underlying dataset's uuid! 'uuid', - # should be: - #'dataset_uuid', + 'permissions', - 'file_path', 'display_apps', 'display_types', 'visualizations', @@ -415,173 +296,109 @@ class HDASerializer( datasets.DatasetAssociationSerializer, annotatable.AnnotatableSerializerMixin.add_serializers( self ) self.serializers.update({ - 'model_class' : lambda *a: 'HistoryDatasetAssociation', - # TODO: accessible needs to go away - 'accessible' : lambda *a: True, + 'model_class' : lambda *a, **c: 'HistoryDatasetAssociation', + 'history_content_type': lambda *a, **c: 'dataset', + 'hda_ldda' : lambda *a, **c: 'hda', + 'type_id' : self.serialize_type_id, - 'id' : self.serialize_id, 'history_id' : self.serialize_id, - 'history_content_type': lambda *a: 'dataset', - 'dataset_id' : self.serialize_id, - 'hda_ldda' : lambda *a: 'hda', # remapped - 'info' : lambda t, i, k: i.info.strip() if isinstance( i.info, basestring ) else '', - 'misc_info' : lambda t, i, k: self.serializers[ 'info' ]( t, i, k ), - 'misc_blurb' : lambda t, i, k: i.blurb, - 'genome_build' : lambda t, i, k: i.dbkey, - 'file_ext' : lambda t, i, k: i.extension, - 'file_size' : lambda t, i, k: self.serializers[ 'size' ]( t, i, k ), + 'misc_info' : self._remap_from( 'info' ), + 'misc_blurb' : self._remap_from( 'blurb' ), + 'file_ext' : self._remap_from( 'extension' ), + 'file_path' : self._remap_from( 'file_name' ), - 'create_time' : self.serialize_date, - 'update_time' : self.serialize_date, - 'copied_from_history_dataset_association_id' : self.serialize_id, - 'copied_from_library_dataset_dataset_association_id': self.serialize_id, + 'resubmitted' : lambda i, k, **c: self.hda_manager.has_been_resubmitted( i ), - 'resubmitted' : lambda t, i, k: self.hda_manager.has_been_resubmitted( i ), - - 'meta_files' : self.serialize_meta_files, - 'file_path' : self.serialize_file_path, - - 'size' : lambda t, i, k: int( i.get_size() ), - 'nice_size' : lambda t, i, k: i.get_size( nice_size=True ), - 'data_type' : lambda t, i, k: i.datatype.__class__.__module__ + '.' + i.datatype.__class__.__name__, - 'peek' : lambda t, i, k: i.display_peek() if i.peek and i.peek != 'no peek' else None, - - # currently we send this sub-obj flattened (see serialize and add_flattened_metadata below) - # 'metadata' : self.serialize_metadata, - # make it available here under a different key - 'metadata_dict' : self.serialize_metadata, - - 'parent_id' : self.serialize_id, - 'annotation' : self.serialize_annotation, - 'tags' : self.serialize_tags, - - 'display_apps' : lambda t, i, k: self.hda_manager.get_display_apps( t, i ), - 'display_types' : lambda t, i, k: self.hda_manager.get_old_display_applications( t, i ), - 'visualizations': lambda t, i, k: self.hda_manager.get_visualizations( t, i ), - - 'dataset_uuid' : lambda t, i, k: str( i.dataset.uuid ) if i.dataset.uuid else None, - 'uuid' : lambda t, i, k: self.serializers[ 'dataset_uuid' ]( t, i, k ), + 'display_apps' : self.serialize_display_apps, + 'display_types' : self.serialize_old_display_applications, + 'visualizations': self.serialize_visualization_links, # 'url' : url_for( 'history_content_typed', history_id=encoded_history_id, id=encoded_id, type="dataset" ), # TODO: this intermittently causes a routes.GenerationException - temp use the legacy route to prevent this # see also: https://trello.com/c/5d6j4X5y # see also: https://sentry.galaxyproject.org/galaxy/galaxy-main/group/20769/events/9352883/ - 'url' : lambda t, i, k: self.url_for( 'history_content', - history_id=t.security.encode_id( i.history_id ), id=t.security.encode_id( i.id ) ), - + 'url' : lambda i, k, **c: self.url_for( 'history_content', + history_id=self.app.security.encode_id( i.history_id ), + id=self.app.security.encode_id( i.id ) ), 'urls' : self.serialize_urls, - 'download_url' : lambda t, i, k: self.url_for( 'history_contents_display', - history_id=t.security.encode_id( i.history.id ), - history_content_id=t.security.encode_id( i.id ) ), - 'api_type' : lambda *a: 'file', - 'type' : lambda *a: 'file' + # TODO: backwards compat: need to go away + 'download_url' : lambda i, k, **c: self.url_for( 'history_contents_display', + history_id=self.app.security.encode_id( i.history.id ), + history_content_id=self.app.security.encode_id( i.id ) ), + 'parent_id' : self.serialize_id, + 'accessible' : lambda *a, **c: True, + 'api_type' : lambda *a, **c: 'file', + 'type' : lambda *a, **c: 'file' }) - def serialize( self, trans, hda, keys ): - """ - Override to add metadata as flattened keys on the serialized HDA. - """ - # if 'metadata' isn't removed from keys here serialize will retrieve the un-serializable MetadataCollection - # TODO: remove these when metadata is sub-object - KEYS_HANDLED_SEPARATELY = ( 'metadata', ) - left_to_handle = self.pluck_from_list( keys, KEYS_HANDLED_SEPARATELY ) - serialized = super( HDASerializer, self ).serialize( trans, hda, keys ) + def serialize_type_id( self, hda, key, **context ): + return 'dataset-' + self.serializers[ 'id' ]( hda, 'id' ) - if 'metadata' in left_to_handle: - # we currently add metadata directly to the dict instead of as a sub-object - metadata = self.prefixed_metadata( trans, hda ) - serialized.update( metadata ) - return serialized + def serialize_display_apps( self, hda, key, trans=None, **context ): + """ + Return dictionary containing new-style display app urls. + """ + display_apps = [] + for display_app in hda.get_display_applications( trans ).itervalues(): - # TODO: this is more util/gen. use - def pluck_from_list( self, l, elems ): - """ - Removes found elems from list l and returns list of found elems if found. - """ - found = [] - for elem in elems: - try: - index = l.index( elem ) - found.append( l.pop( index ) ) - except ValueError: - pass - return found + app_links = [] + for link_app in display_app.links.itervalues(): + app_links.append({ + 'target': link_app.url.get( 'target_frame', '_blank' ), + 'href': link_app.get_display_url( hda, trans ), + 'text': gettext.gettext( link_app.name ) + }) + if app_links: + display_apps.append( dict( label=display_app.name, links=app_links ) ) - def prefixed_metadata( self, trans, hda ): - """ - Adds (a prefixed version of) the hda metadata to the dict, prefixing each key - with 'metadata_'. - """ - metadata = self.serialize_metadata( trans, hda, 'metadata' ) - # TODO: this is factored out for removal - metadata should be a sub-object instead: - # i.e. 'metadata' : self.serialize_metadata, - prefixed = {} - for key, val in metadata.items(): - prefixed_key = 'metadata_' + key - prefixed[ prefixed_key ] = val - return prefixed + return display_apps - def serialize_metadata( self, trans, hda, key, excluded=None ): + def serialize_old_display_applications( self, hda, key, trans=None, **context ): """ - Cycle through metadata and return as dictionary. + Return dictionary containing old-style display app urls. """ - # dbkey is a repeat actually (metadata_dbkey == genome_build) - # excluded = [ 'dbkey' ] if excluded is None else excluded - excluded = [] if excluded is None else excluded + display_apps = [] + if not self.app.config.enable_old_display_applications: + return display_apps - metadata = {} - for name, spec in hda.metadata.spec.items(): - if name in excluded: - continue - val = hda.metadata.get( name ) - # NOTE: no files - if isinstance( val, model.MetadataFile ): - # only when explicitly set: fetching filepaths can be expensive - if not self.app.config.expose_dataset_path: - continue - val = val.file_name - # TODO:? possibly split this off? - # If no value for metadata, look in datatype for metadata. - elif val is None and hasattr( hda.datatype, name ): - val = getattr( hda.datatype, name ) - metadata[ name ] = val + display_link_fn = hda.datatype.get_display_links + for display_app in hda.datatype.get_display_types(): + target_frame, display_links = display_link_fn( hda, display_app, self.app, trans.request.base ) - return metadata + if len( display_links ) > 0: + display_label = hda.datatype.get_display_label( display_app ) - # add to serialize_metadata above - def serialize_meta_files( self, trans, hda, key ): - """ - Cycle through meta files and return them as a list of dictionaries. - """ - meta_files = [] - for meta_type in hda.metadata.spec.keys(): - if isinstance( hda.metadata.spec[ meta_type ].param, galaxy.datatypes.metadata.FileParameter ): - meta_files.append( dict( file_type=meta_type ) ) - return meta_files + app_links = [] + for display_name, display_link in display_links: + app_links.append({ + 'target': target_frame, + 'href': display_link, + 'text': gettext.gettext( display_name ) + }) + if app_links: + display_apps.append( dict( label=display_label, links=app_links ) ) - # def file_info #TODO - # TODO: and to dataset instead (passing through object store) - def serialize_file_path( self, trans, hda, key ): - """ - Return the `file_name` of the HDA if the config exposes it, None otherwise. - """ - # TODO: allow admin - if self.app.config.expose_dataset_path: - try: - return hda.file_name - except objectstore.ObjectNotFound: - log.exception( 'objectstore.ObjectNotFound, HDA %s.', hda.id ) - return None + return display_apps - def serialize_urls( self, trans, hda, key ): + def serialize_visualization_links( self, hda, key, trans=None, **context ): + """ + Return a list of dictionaries with links to visualization pages + for those visualizations that apply to this hda. + """ + # use older system if registry is off in the config + if not self.app.visualizations_registry: + return hda.get_visualizations() + return self.app.visualizations_registry.get_visualizations( trans, hda ) + + def serialize_urls( self, hda, key, **context ): """ Return web controller urls useful for this HDA. """ url_for = self.url_for - encoded_id = self.security.encode_id( hda.id ) + encoded_id = self.app.security.encode_id( hda.id ) urls = { 'purge' : url_for( controller='dataset', action='purge_async', dataset_id=encoded_id ), 'display' : url_for( controller='dataset', action='display', dataset_id=encoded_id, preview=True ), @@ -617,41 +434,21 @@ class HDADeserializer( datasets.DatasetAssociationDeserializer, annotatable.AnnotatableDeserializerMixin.add_deserializers( self ) self.deserializers.update({ - 'name' : self.deserialize_basestring, 'visible' : self.deserialize_bool, - # remapped - 'genome_build' : lambda t, i, k, v: self.deserialize_genome_build( t, i, 'dbkey', v ), - 'misc_info' : lambda t, i, k, v: ( - self.deserialize_basestring( t, i, 'info', v, convert_none_to_empty=True ) ), - - # TODO: mixin: deletable - 'deleted' : self.deserialize_bool, - # sharable - 'published' : self.deserialize_bool, - 'importable' : self.deserialize_bool, - + 'genome_build' : lambda i, k, v, **c: self.deserialize_genome_build( i, 'dbkey', v ), + 'misc_info' : lambda i, k, v, **c: self.deserialize_basestring( i, 'info', v, + convert_none_to_empty=True ), }) self.deserializable_keyset.update( self.deserializers.keys() ) -class HDAFilters( datasets.DatasetAssociationFilters ): +class HDAFilterParser( datasets.DatasetAssociationFilterParser, + taggable.TaggableFilterMixin, + annotatable.AnnotatableFilterMixin ): model_class = model.HistoryDatasetAssociation def _add_parsers( self ): - super( HDAFilters, self )._add_parsers() - - self.fn_filter_parsers.update({ - # TODO: filter_string_attr_contains, filter_string_attr_eq - # 'dbkey' : { 'op': { 'has' : self.filter_annotation_contains, } }, - - # TODO: add this in annotatable mixin - 'annotation': { 'op': { 'has': self.filter_annotation_contains, } }, - # TODO: add this in taggable mixin - 'tag': { - 'op': { - 'eq': self.filter_has_tag, - 'has': self.filter_has_partial_tag, - } - } - }) + super( HDAFilterParser, self )._add_parsers() + taggable.TaggableFilterMixin._add_parsers( self ) + annotatable.AnnotatableFilterMixin._add_parsers( self ) diff --git a/lib/galaxy/managers/histories.py b/lib/galaxy/managers/histories.py index c50f69d5827..b167e07a0e6 100644 --- a/lib/galaxy/managers/histories.py +++ b/lib/galaxy/managers/histories.py @@ -9,7 +9,7 @@ from galaxy import model from galaxy.managers import sharable from galaxy.managers import deletable from galaxy.managers import hdas -from galaxy.managers.collections_util import dictify_dataset_collection_instance +from galaxy.managers import collections_util import logging log = logging.getLogger( __name__ ) @@ -32,7 +32,7 @@ class HistoryManager( sharable.SharableModelManager, deletable.PurgableManagerMi self.hda_manager = hdas.HDAManager( app ) - def copy( self, trans, history, user, **kwargs ): + def copy( self, history, user, **kwargs ): """ Copy and return the given `history`. """ @@ -40,60 +40,62 @@ class HistoryManager( sharable.SharableModelManager, deletable.PurgableManagerMi # .... sharable # overriding to handle anonymous users' current histories in both cases - def by_user( self, trans, user, **kwargs ): + def by_user( self, user, current_history=None, **kwargs ): """ Get all the histories for a given user (allowing anon users' theirs) ordered by update time. """ # handle default and/or anonymous user (which still may not have a history yet) if self.user_manager.is_anonymous( user ): - current_history = self.get_current( trans ) return [ current_history ] if current_history else [] + return super( HistoryManager, self ).by_user( user, **kwargs ) - return super( HistoryManager, self ).by_user( trans, user, **kwargs ) - - def is_owner( self, trans, history, user ): + def is_owner( self, history, user, current_history=None, **kwargs ): """ True if the current user is the owner of the given history. """ # anon users are only allowed to view their current history - if self.user_manager.is_anonymous( user ) and history == self.get_current( trans ): - return True - return super( HistoryManager, self ).is_owner( trans, history, user ) + if self.user_manager.is_anonymous( user ): + if current_history and history == current_history: + return True + return False + return super( HistoryManager, self ).is_owner( history, user ) - # TODO: possibly to sharable - def most_recent( self, trans, user, filters=None, **kwargs ): + # TODO: possibly to sharable or base + def most_recent( self, user, filters=None, current_history=None, **kwargs ): """ Return the most recently update history for the user. If user is anonymous, return the current history. If the user is anonymous and the current history is deleted, return None. """ - # TODO: trans - if not user: - current_history = self.get_current( trans ) + if self.user_manager.is_anonymous( user ): return None if ( not current_history or current_history.deleted ) else current_history desc_update_time = self.model_class.table.c.update_time filters = self._munge_filters( filters, self.model_class.user_id == user.id ) # TODO: normalize this return value - return self.query( trans, filters=filters, order_by=desc_update_time, limit=1, **kwargs ).first() + return self.query( filters=filters, order_by=desc_update_time, limit=1, **kwargs ).first() # .... purgable - def purge( self, trans, history, flush=True, **kwargs ): + def purge( self, history, flush=True, **kwargs ): """ Purge this history and all HDAs, Collections, and Datasets inside this history. """ - self.hda_manager.dataset_manager.error_unless_dataset_purge_allowed( trans, history ) + self.hda_manager.dataset_manager.error_unless_dataset_purge_allowed() # First purge all the datasets for hda in history.datasets: if not hda.purged: - self.hda_manager.purge( trans, hda, flush=True ) + self.hda_manager.purge( hda, flush=True ) # Now mark the history as purged - super( HistoryManager, self ).purge( trans, history, flush=flush, **kwargs ) + super( HistoryManager, self ).purge( history, flush=flush, **kwargs ) # .... current + # TODO: make something to bypass the anon user + current history permissions issue + # def is_current_users_current_history( self, history, trans ): + # pass + def get_current( self, trans ): """ Return the current history. @@ -113,10 +115,9 @@ class HistoryManager( sharable.SharableModelManager, deletable.PurgableManagerMi """ Set the current history by an id. """ - return self.set_current( trans, self.by_id( trans, history_id ) ) + return self.set_current( trans, self.by_id( history_id ) ) - # .... serialization - # TODO: move to serializer (i.e. history with contents attr) +# TODO: replace or move to serializer def _get_history_data( self, trans, history ): """ Returns a dictionary containing ``history`` and ``contents``, serialized @@ -128,23 +129,27 @@ class HistoryManager( sharable.SharableModelManager, deletable.PurgableManagerMi history_dictionary = {} contents_dictionaries = [] try: - history_dictionary = history_serializer.serialize_to_view( trans, history, view='detailed' ) + history_dictionary = history_serializer.serialize_to_view( history, view='detailed', + user=trans.user, trans=trans ) + for content in history.contents_iter( types=[ 'dataset', 'dataset_collection' ] ): contents_dict = {} if isinstance( content, model.HistoryDatasetAssociation ): - contents_dict = hda_serializer.serialize_to_view( trans, content, view='detailed' ) + contents_dict = hda_serializer.serialize_to_view( content, view='detailed', + user=trans.user, trans=trans ) elif isinstance( content, model.HistoryDatasetCollectionAssociation ): try: service = self.app.dataset_collections_service - dataset_collection_instance = service.get_dataset_collection_instance( + collection = service.get_dataset_collection_instance( trans=trans, instance_type='history', id=self.app.security.encode_id( content.id ), ) - contents_dict = dictify_dataset_collection_instance( dataset_collection_instance, - security=self.app.security, - parent=dataset_collection_instance.history, - view="element" ) + serializer = collections_util.dictify_dataset_collection_instance + contents_dict = serializer( collection, + security=self.app.security, + parent=collection.history, + view="element" ) except Exception, exc: log.exception( "Error in history API at listing dataset collection: %s", exc ) # TODO: return some dict with the error @@ -160,79 +165,6 @@ class HistoryManager( sharable.SharableModelManager, deletable.PurgableManagerMi return { 'history': history_dictionary, 'contents': contents_dictionaries } - # remove this - def get_state_counts( self, trans, history, exclude_deleted=True, exclude_hidden=False ): - """ - Return a dictionary keyed to possible dataset states and valued with the number - of datasets in this history that have those states. - """ - # TODO: the default flags above may not make a lot of sense (T,T?) - state_counts = {} - for state in model.Dataset.states.values(): - state_counts[ state ] = 0 - - # TODO:?? collections and coll. states? - for hda in history.datasets: - if exclude_deleted and hda.deleted: - continue - if exclude_hidden and not hda.visible: - continue - state_counts[ hda.state ] = state_counts[ hda.state ] + 1 - return state_counts - - # remove this - def get_state_ids( self, trans, history ): - """ - Return a dictionary keyed to possible dataset states and valued with lists - containing the ids of each HDA in that state. - """ - state_ids = {} - for state in model.Dataset.states.values(): - state_ids[ state ] = [] - - # TODO:?? collections and coll. states? - for hda in history.datasets: - # TODO: do not encode ids at this layer - encoded_id = self.app.security.encode_id( hda.id ) - state_ids[ hda.state ].append( encoded_id ) - return state_ids - - # TODO: remove this (is state used/useful?) - def get_history_state( self, trans, history ): - """ - Returns the history state based on the states of the HDAs it contains. - """ - states = model.Dataset.states - - # (default to ERROR) - state = states.ERROR - - # TODO: history_state and state_counts are classically calc'd at the same time - # so this is rel. ineff. - if we keep this... - hda_state_counts = self.get_state_counts( trans, history, exclude_deleted=False ) - num_hdas = sum( hda_state_counts.values() ) - if num_hdas == 0: - state = states.NEW - - else: - if ( hda_state_counts[ states.RUNNING ] > 0 - or hda_state_counts[ states.SETTING_METADATA ] > 0 - or hda_state_counts[ states.UPLOAD ] > 0 ): - state = states.RUNNING - # TODO: this method may be more useful if we *also* polled the histories jobs here too - - elif hda_state_counts[ states.QUEUED ] > 0: - state = states.QUEUED - - elif ( hda_state_counts[ states.ERROR ] > 0 - or hda_state_counts[ states.FAILED_METADATA ] > 0 ): - state = states.ERROR - - elif hda_state_counts[ states.OK ] == num_hdas: - state = states.OK - - return state - class HistorySerializer( sharable.SharableModelSerializer, deletable.PurgableSerializerMixin ): """ @@ -283,48 +215,115 @@ class HistorySerializer( sharable.SharableModelSerializer, deletable.PurgableSer deletable.PurgableSerializerMixin.add_serializers( self ) self.serializers.update({ - 'model_class' : lambda *a: 'History', + 'model_class' : lambda *a, **c: 'History', 'id' : self.serialize_id, 'create_time' : self.serialize_date, 'update_time' : self.serialize_date, - 'size' : lambda t, i, k: int( i.get_disk_size() ), - 'nice_size' : lambda t, i, k: i.get_disk_size( nice_size=True ), - 'state' : lambda t, i, k: self.history_manager.get_history_state( t, i ), + 'size' : lambda i, k, **c: int( i.get_disk_size() ), + 'nice_size' : lambda i, k, **c: i.get_disk_size( nice_size=True ), + 'state' : self.serialize_history_state, - 'url' : lambda t, i, k: self.url_for( 'history', id=t.security.encode_id( i.id ) ), - 'contents_url' : lambda t, i, k: self.url_for( 'history_contents', - history_id=t.security.encode_id( i.id ) ), + 'url' : lambda i, k, **c: self.url_for( 'history', id=self.app.security.encode_id( i.id ) ), + 'contents_url' : lambda i, k, **c: self.url_for( 'history_contents', + history_id=self.app.security.encode_id( i.id ) ), - 'empty' : lambda t, i, k: ( len( i.datasets ) + len( i.dataset_collections ) ) <= 0, - 'count' : lambda trans, item, key: len( item.datasets ), - 'hdas' : lambda t, i, k: [ t.security.encode_id( hda.id ) for hda in i.datasets ], - 'state_details' : lambda t, i, k: self.history_manager.get_state_counts( t, i ), - 'state_ids' : lambda t, i, k: self.history_manager.get_state_ids( t, i ), + 'empty' : lambda i, k, **c: ( len( i.datasets ) + len( i.dataset_collections ) ) <= 0, + 'count' : lambda i, k, **c: len( i.datasets ), + 'hdas' : lambda i, k, **c: [ self.app.security.encode_id( hda.id ) for hda in i.datasets ], + 'state_details' : self.serialize_state_counts, + 'state_ids' : self.serialize_state_ids, 'contents' : self.serialize_contents }) - def serialize_contents( self, trans, history, *args ): + # remove this + def serialize_state_ids( self, history, key, **context ): + """ + Return a dictionary keyed to possible dataset states and valued with lists + containing the ids of each HDA in that state. + """ + state_ids = {} + for state in model.Dataset.states.values(): + state_ids[ state ] = [] + + # TODO:?? collections and coll. states? + for hda in history.datasets: + # TODO: do not encode ids at this layer + encoded_id = self.app.security.encode_id( hda.id ) + state_ids[ hda.state ].append( encoded_id ) + return state_ids + + # remove this + def serialize_state_counts( self, history, key, exclude_deleted=True, exclude_hidden=False, **context ): + """ + Return a dictionary keyed to possible dataset states and valued with the number + of datasets in this history that have those states. + """ + # TODO: the default flags above may not make a lot of sense (T,T?) + state_counts = {} + for state in model.Dataset.states.values(): + state_counts[ state ] = 0 + + # TODO:?? collections and coll. states? + for hda in history.datasets: + if exclude_deleted and hda.deleted: + continue + if exclude_hidden and not hda.visible: + continue + state_counts[ hda.state ] = state_counts[ hda.state ] + 1 + return state_counts + + # TODO: remove this (is state used/useful?) + def serialize_history_state( self, history, key, **context ): + """ + Returns the history state based on the states of the HDAs it contains. + """ + states = model.Dataset.states + # (default to ERROR) + state = states.ERROR + # TODO: history_state and state_counts are classically calc'd at the same time + # so this is rel. ineff. - if we keep this... + hda_state_counts = self.serialize_state_counts( history, 'counts', exclude_deleted=False, **context ) + num_hdas = sum( hda_state_counts.values() ) + if num_hdas == 0: + state = states.NEW + + else: + if ( hda_state_counts[ states.RUNNING ] > 0 + or hda_state_counts[ states.SETTING_METADATA ] > 0 + or hda_state_counts[ states.UPLOAD ] > 0 ): + state = states.RUNNING + # TODO: this method may be more useful if we *also* polled the histories jobs here too + elif hda_state_counts[ states.QUEUED ] > 0: + state = states.QUEUED + elif ( hda_state_counts[ states.ERROR ] > 0 + or hda_state_counts[ states.FAILED_METADATA ] > 0 ): + state = states.ERROR + elif hda_state_counts[ states.OK ] == num_hdas: + state = states.OK + + return state + + def serialize_contents( self, history, *args, **context ): contents_dictionaries = [] for content in history.contents_iter( types=[ 'dataset', 'dataset_collection' ] ): contents_dict = {} if isinstance( content, model.HistoryDatasetAssociation ): - contents_dict = self.hda_serializer.serialize_to_view( trans, content, view='detailed' ) - elif isinstance( content, model.HistoryDatasetCollectionAssociation ): - contents_dict = self.serialize_collection( trans, content ) + contents_dict = self.hda_serializer.serialize_to_view( content, view='detailed', **context ) + # elif isinstance( content, model.HistoryDatasetCollectionAssociation ): + # contents_dict = self._serialize_collection( trans, content ) contents_dictionaries.append( contents_dict ) return contents_dictionaries - def serialize_collection( self, trans, collection ): + # TODO: remove trans + def _serialize_collection( self, trans, collection ): service = self.app.dataset_collections_service dataset_collection_instance = service.get_dataset_collection_instance( trans=trans, instance_type='history', - id=self.security.encode_id( collection.id ), + id=self.app.security.encode_id( collection.id ), ) - return dictify_dataset_collection_instance( dataset_collection_instance, - security=self.app.security, - parent=dataset_collection_instance.history, - view="element" ) + return collections_util.dictify_dataset_collection_instance( dataset_collection_instance, + security=self.app.security, parent=dataset_collection_instance.history, view="element" ) class HistoryDeserializer( sharable.SharableModelDeserializer, deletable.PurgableDeserializerMixin ): @@ -342,33 +341,20 @@ class HistoryDeserializer( sharable.SharableModelDeserializer, deletable.Purgabl deletable.PurgableDeserializerMixin.add_deserializers( self ) self.deserializers.update({ - 'name': self.deserialize_basestring, - 'genome_build': self.deserialize_genome_build, + 'name' : self.deserialize_basestring, + 'genome_build' : self.deserialize_genome_build, }) -class HistoryFilters( sharable.SharableModelFilters, deletable.PurgableFiltersMixin ): +class HistoryFilters( sharable.SharableModelFilters, + deletable.PurgableFiltersMixin ): model_class = model.History def _add_parsers( self ): super( HistoryFilters, self )._add_parsers() deletable.PurgableFiltersMixin._add_parsers( self ) - self.orm_filter_parsers.update({ # history specific - 'name': { 'op': ( 'eq', 'contains', 'like' ) }, - 'genome_build': { 'op': ( 'eq', 'contains', 'like' ) }, - }) - - # TODO: I'm not entirely convinced this (or tags) are a good idea for filters since they involve a/the user - self.fn_filter_parsers.update({ - # TODO: add this in annotatable mixin - 'annotation': { 'op': { 'has': self.filter_annotation_contains, } }, - # TODO: add this in taggable mixin - 'tag': { - 'op': { - 'eq': self.filter_has_tag, - 'has': self.filter_has_partial_tag, - } - } + 'name' : { 'op': ( 'eq', 'contains', 'like' ) }, + 'genome_build' : { 'op': ( 'eq', 'contains', 'like' ) }, }) diff --git a/lib/galaxy/managers/ratable.py b/lib/galaxy/managers/ratable.py index b6682a58222..21e6e0f3884 100644 --- a/lib/galaxy/managers/ratable.py +++ b/lib/galaxy/managers/ratable.py @@ -26,12 +26,12 @@ class RatableSerializerMixin( object ): self.serializers[ 'user_rating' ] = self.serialize_user_rating self.serializers[ 'community_rating' ] = self.serialize_community_rating - def serialize_user_rating( self, trans, item, key ): + def serialize_user_rating( self, item, key, user=None, **context ): """ """ pass - def serialize_community_rating( self, trans, item, key ): + def serialize_community_rating( self, item, key, **context ): """ """ pass diff --git a/lib/galaxy/managers/rbac_secured.py b/lib/galaxy/managers/rbac_secured.py new file mode 100644 index 00000000000..1391c1c4cc1 --- /dev/null +++ b/lib/galaxy/managers/rbac_secured.py @@ -0,0 +1,255 @@ +from galaxy import security +import galaxy.exceptions +from galaxy import model +from galaxy.managers import users + +import logging +log = logging.getLogger( __name__ ) + +class RBACPermissionFailedException( galaxy.exceptions.InsufficientPermissionsException ): pass + +class RBACPermission( object ): + """ + Base class for wrangling/controlling the permissions ORM models (*Permissions, Roles) + that control which users can perform certain actions on their associated models + (Libraries, Datasets). + """ + + permissions_class = None + permission_failed_error_class = RBACPermissionFailedException + + def __init__( self, app ): + self.app = app + self.user_manager = users.UserManager( app ) + + def session( self ): + return self.app.model.context + + # TODO: implement group + # TODO: how does admin play into this? + def is_permitted( self, item, user ): + raise NotImplementedError( "abstract parent class" ) + + def error_unless_permitted( self, item, user ): + if not self.is_permitted( item, user ): + error_info = dict( model_class=item.__class__, id=getattr( item, 'id', None ) ) + raise self.permission_failed_error_class( **error_info ) + + def grant( self, item, user, flush=True ): + raise NotImplementedError( "abstract parent class" ) + + def revoke( self, item, user, flush=True ): + raise NotImplementedError( "abstract parent class" ) + + def _role_is_permitted( self, item, role ): + raise NotImplementedError( "abstract parent class" ) + + def _error_unless_role_permitted( self, item, role ): + if not self._role_is_permitted( item, role ): + error_info = dict( model_class=item.__class__, id=getattr( item, 'id', None ) ) + raise self.permission_failed_error_class( **error_info ) + + def _grant_role( self, item, role, flush=True ): + raise NotImplementedError( "abstract parent class" ) + + def _revoke_role( self, item, role, flush=True ): + raise NotImplementedError( "abstract parent class" ) + +class DatasetRBACPermission( RBACPermission ): + """ + Base class for the manage and access RBAC permissions used by dataset security. + + The DatasetPermissions used by the RBAC agent are associations between a Dataset + and a single Role. + + DatasetPermissions are typed (but not polymorphic themselves) by a string 'action'. + There are two types: + - manage permissions : can a role manage the permissions on a dataset + - access : can a role read/look at/copy a dataset + """ + permissions_class = model.DatasetPermissions + action_name = None + + # ---- double secrect probation + def __assert_action( self ): + if not self.action_name: + raise NotImplementedError( "abstract parent class" + " needs action_name" ) + + # ---- interface + def by_dataset( self, dataset ): + self.__assert_action() + all_permissions = self._all_types_by_dataset( dataset ) + return filter( lambda p: p.action == self.action_name, all_permissions ) + +#TODO: list? + def by_roles( self, dataset, roles ): + permissions = self.by_dataset( dataset ) + return filter( lambda p: p.role in roles, permissions ) + + def by_role( self, dataset, role ): + permissions = self.by_dataset( dataset ) + found = filter( lambda p: p.role == role, permissions ) + if not found: + return None + if len( found ) > 1: + raise galaxy.exceptions.InconsistentDatabase( dataset=dataset.id, role=role.id ) + return found[0] + + def set( self, dataset, roles, flush=True ): + # NOTE: this removes all previous permissions of this type + self.clear( dataset, flush=False ) + permissions = [] + for role in roles: + permissions.append( self._create( dataset, role, flush=False ) ) + if flush: + self.session().flush() + return permissions + + def clear( self, dataset, flush=True ): + permissions = self.by_dataset( dataset ) + return self._delete( permissions, flush=flush ) + + # ---- private + def _create( self, dataset, role, flush=True ): + permission = self.permissions_class( self.action_name, dataset, role ) + self.session().add( permission ) + if flush: + self.session().flush() + return permission + + def _roles( self, dataset ): + return [ permission.role for permission in self.by_dataset( dataset ) ] + + def _all_types_by_dataset( self, dataset ): + return dataset.actions + + # as a general rule, DatasetPermissions are considered disposable + # and there is no reason to update the models + +#TODO: list? + def _delete( self, permissions, flush=True ): + for permission in permissions: + self.session().delete( permission ) + if flush: + self.session().flush() + + def _revoke_role( self, dataset, role, flush=True ): + role_permissions = self.by_roles( dataset, [ role ] ) + return self._delete( role_permissions, flush=flush ) + + +def iterable_has_all( iterable, has_these ): + for item in has_these: + if item not in iterable: + return False + return True + + +class DatasetManagePermissionFailedException( RBACPermissionFailedException ): pass + + +class ManageDatasetRBACPermission( DatasetRBACPermission ): + """ + A class that controls the dataset permissions that control + who can manage that dataset's permissions. + + When checking permissions for a user, if any of the user's roles + have permission on the dataset + """ + # TODO: We may also be able to infer/record the dataset 'owner' as well. + action_name = security.RBACAgent.permitted_actions.DATASET_MANAGE_PERMISSIONS.action + permission_failed_error_class = DatasetManagePermissionFailedException + + # ---- interface + def is_permitted( self, dataset, user ): + # anonymous users cannot manage permissions on datasets + if self.user_manager.is_anonymous( user ): + return False + for role in user.all_roles(): + if self._role_is_permitted( dataset, role ): + return True + return False + + def grant( self, dataset, user, flush=True ): + private_role = self._user_private_role( user ) + return self._grant_role( dataset, private_role, flush=flush ) + + def revoke( self, dataset, user, flush=True ): + private_role = self._user_private_role( user ) + return self._revoke_role( dataset, private_role, flush=flush ) + + # ---- private + def _role_is_permitted( self, dataset, role ): + return role in self._roles( dataset ) + + def _user_private_role( self, user ): + # error with 401 if no user + self.user_manager.error_if_anonymous( user ) + return self.user_manager.private_role( user ) + + def _grant_role( self, dataset, role, flush=True ): + existing = self.by_role( dataset, role ) + if existing: + return existing + return self._create( dataset, role, flush=flush ) + + def _revoke_role( self, dataset, role, flush=True ): + permission = self.by_roles( dataset, [ role ] ) + return self._delete( [ permission ], flush=flush ) + + +class DatasetAccessPermissionFailedException( RBACPermissionFailedException ): pass + + +class AccessDatasetRBACPermission( DatasetRBACPermission ): + """ + A class to manage access permissions on a dataset. + + An user must have all the Roles of all the access permissions associated + with a dataset in order to access it. + """ + action_name = security.RBACAgent.permitted_actions.DATASET_ACCESS.action + permission_failed_error_class = DatasetAccessPermissionFailedException + + # ---- interface + def is_permitted( self, dataset, user ): + current_roles = self._roles( dataset ) + # NOTE: that because of short circuiting this allows + # anonymous access to public datasets + return ( self._is_public_from_roles( current_roles ) + or self._user_has_all_roles( user, current_roles ) ) + + def grant( self, item, user ): + pass + # not so easy + # need to check for a sharing role + # then add the new user to it + + def revoke( self, item, user ): + pass + # not so easy + + # TODO: these are a lil off message + def is_public( self, dataset ): + current_roles = self._roles( dataset ) + return self._is_public_from_roles( current_roles ) + + def set_private( self, dataset, user, flush=True ): + private_role = self.user_manager.private_role( user ) + return self.set( dataset, [ private_role ], flush=flush ) + + # ---- private + def _is_public_from_roles( self, roles ): + return len( roles ) == 0 + + def _user_has_all_roles( self, user, roles ): + user_roles = [] + if not self.user_manager.is_anonymous( user ): + user_roles = user.all_roles() + return iterable_has_all( user_roles, roles ) + + def _role_is_permitted( self, dataset, role ): + current_roles = self._roles( dataset ) + return ( self._is_public_from_roles( current_roles ) + # if there's only one role and this is it, let em in + or ( ( len( current_roles ) == 1 ) and ( role == current_roles[0] ) ) ) diff --git a/lib/galaxy/managers/roles.py b/lib/galaxy/managers/roles.py index 80bbbced422..1cae158d922 100644 --- a/lib/galaxy/managers/roles.py +++ b/lib/galaxy/managers/roles.py @@ -2,40 +2,49 @@ Manager and Serializer for Roles. """ -import galaxy.web -from galaxy import exceptions -from galaxy.model import orm -from sqlalchemy.orm.exc import MultipleResultsFound -from sqlalchemy.orm.exc import NoResultFound +from sqlalchemy.orm import exc as sqlalchemy_exceptions + +import galaxy.exceptions +from galaxy import model +from galaxy.managers import base import logging log = logging.getLogger( __name__ ) # ============================================================================= -class RoleManager( object ): +class RoleManager( base.ModelManager ): """ - Interface/service object for interacting with folders. + Business logic for roles. """ + model_class = model.Role + foreign_key_name = 'role' - def get( self, trans, decoded_role_id): - """ - Method loads the role from the DB based on the given role id. + user_assoc = model.UserRoleAssociation + group_assoc = model.GroupRoleAssociation - :param decoded_role_id: id of the role to load from the DB - :type decoded_role_id: int + def __init__( self, app ): + super( RoleManager, self ).__init__( app ) - :returns: the loaded Role object - :rtype: Role + def get( self, trans, decoded_role_id ): + """ + Method loads the role from the DB based on the given role id. - :raises: InconsistentDatabase, RequestParameterInvalidException, InternalServerError - """ - try: - role = trans.sa_session.query( trans.app.model.Role ).filter( trans.model.Role.table.c.id == decoded_role_id ).one() - except MultipleResultsFound: - raise exceptions.InconsistentDatabase( 'Multiple roles found with the same id.' ) - except NoResultFound: - raise exceptions.RequestParameterInvalidException( 'No role found with the id provided.' ) - except Exception, e: - raise exceptions.InternalServerError( 'Error loading from the database.' + str(e) ) - return role + :param decoded_role_id: id of the role to load from the DB + :type decoded_role_id: int + + :returns: the loaded Role object + :rtype: Role + + :raises: InconsistentDatabase, RequestParameterInvalidException, InternalServerError + """ + try: + role = ( self.session().query( self.model_class ) + .filter( self.model_class.id == decoded_role_id ).one() ) + except sqlalchemy_exceptions.MultipleResultsFound: + raise galaxy.exceptions.InconsistentDatabase( 'Multiple roles found with the same id.' ) + except sqlalchemy_exceptions.NoResultFound: + raise galaxy.exceptions.RequestParameterInvalidException( 'No role found with the id provided.' ) + except Exception, e: + raise galaxy.exceptions.InternalServerError( 'Error loading from the database.' + str(e) ) + return role diff --git a/lib/galaxy/managers/secured.py b/lib/galaxy/managers/secured.py index dd6825b4bc3..aada0b0025a 100644 --- a/lib/galaxy/managers/secured.py +++ b/lib/galaxy/managers/secured.py @@ -16,35 +16,35 @@ class AccessibleManagerMixin( object ): """ # don't want to override by_id since consumers will also want to fetch w/o any security checks - def is_accessible( self, trans, item, user ): + def is_accessible( self, item, user, **kwargs ): """ Return True if the item accessible to user. """ # override in subclasses raise exceptions.NotImplemented( "Abstract interface Method" ) - def get_accessible( self, trans, id, user, **kwargs ): + def get_accessible( self, id, user, **kwargs ): """ Return the item with the given id if it's accessible to user, otherwise raise an error. :raises exceptions.ItemAccessibilityException: """ - item = self.by_id( trans, id ) - return self.error_unless_accessible( trans, item, user ) + item = self.by_id( id ) + return self.error_unless_accessible( item, user, **kwargs ) - def error_unless_accessible( self, trans, item, user ): + def error_unless_accessible( self, item, user, **kwargs ): """ Raise an error if the item is NOT accessible to user, otherwise return the item. :raises exceptions.ItemAccessibilityException: """ - if self.is_accessible( trans, item, user ): + if self.is_accessible( item, user, **kwargs ): return item raise exceptions.ItemAccessibilityException( "%s is not accessible by user" % ( self.model_class.__name__ ) ) # TODO:?? are these even useful? - def list_accessible( self, trans, user, **kwargs ): + def list_accessible( self, user, **kwargs ): """ Return a list of items accessible to the user, raising an error if ANY are inaccessible. @@ -56,7 +56,7 @@ class AccessibleManagerMixin( object ): # items = ModelManager.list( self, trans, **kwargs ) # return [ self.error_unless_accessible( trans, item, user ) for item in items ] - def filter_accessible( self, trans, user, **kwargs ): + def filter_accessible( self, user, **kwargs ): """ Return a list of items accessible to the user. """ @@ -76,34 +76,34 @@ class OwnableManagerMixin( object ): This can also be thought of as write/edit privileges. """ - def is_owner( self, trans, item, user ): + def is_owner( self, item, user, **kwargs ): """ Return True if user owns the item. """ # override in subclasses raise exceptions.NotImplemented( "Abstract interface Method" ) - def get_owned( self, trans, id, user, **kwargs ): + def get_owned( self, id, user, **kwargs ): """ Return the item with the given id if owned by the user, otherwise raise an error. :raises exceptions.ItemOwnershipException: """ - item = self.by_id( trans, id ) - return self.error_unless_owner( trans, item, user ) + item = self.by_id( id ) + return self.error_unless_owner( item, user, **kwargs ) - def error_unless_owner( self, trans, item, user ): + def error_unless_owner( self, item, user, **kwargs ): """ Raise an error if the item is NOT owned by user, otherwise return the item. :raises exceptions.ItemAccessibilityException: """ - if self.is_owner( trans, item, user ): + if self.is_owner( item, user, **kwargs ): return item raise exceptions.ItemOwnershipException( "%s is not owned by user" % ( self.model_class.__name__ ) ) - def list_owned( self, trans, user, **kwargs ): + def list_owned( self, user, **kwargs ): """ Return a list of items owned by the user, raising an error if ANY are not. @@ -114,9 +114,9 @@ class OwnableManagerMixin( object ): # just alias to by_user (easier/same thing) #return self.by_user( trans, user, **kwargs ) - def filter_owned( self, trans, user, **kwargs ): + def filter_owned( self, user, **kwargs ): """ Return a list of items owned by the user. """ # just alias to list_owned - return self.list_owned( trans, user, **kwargs ) + return self.list_owned( user, **kwargs ) diff --git a/lib/galaxy/managers/sharable.py b/lib/galaxy/managers/sharable.py index 96b1cc68117..8ecb4944c70 100644 --- a/lib/galaxy/managers/sharable.py +++ b/lib/galaxy/managers/sharable.py @@ -42,30 +42,26 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu self.user_manager = users.UserManager( app ) # .... has a user - def by_user( self, trans, user, filters=None, **kwargs ): + def by_user( self, user, filters=None, **kwargs ): """ Return list for all items (of model_class type) associated with the given `user`. """ user_filter = self.model_class.user_id == user.id filters=self._munge_filters( user_filter, filters ) - return self.list( trans, filters=filters, **kwargs ) + return self.list( filters=filters, **kwargs ) - # .... owned model interface - def is_owner( self, trans, item, user ): + # .... owned/accessible interfaces + def is_owner( self, item, user, **kwargs ): """ Return true if this sharable belongs to `user` (or `user` is an admin). """ - if self.user_manager.is_anonymous( user ): - # note: this needs to be overridden in the case of anon users and session.current_history - return False # ... effectively a good fit to have this here, but not semantically - if self.user_manager.is_admin( trans, user ): + if self.user_manager.is_admin( user ): return True return item.user == user - # .... accessible interface - def is_accessible( self, trans, item, user ): + def is_accessible( self, item, user, **kwargs ): """ If the item is importable, is owned by `user`, or (the valid) `user` is in 'users shared with' list for the item: return True. @@ -73,7 +69,7 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu if item.importable: return True # note: owners always have access - checking for accessible implicitly checks for ownership - if self.is_owner( trans, item, user ): + if self.is_owner( item, user, **kwargs ): return True if self.user_manager.is_anonymous( user ): return False @@ -82,16 +78,16 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu return False # .... importable - def make_importable( self, trans, item, flush=True ): + def make_importable( self, item, flush=True ): """ Makes item accessible--viewable and importable--and sets item's slug. Does not flush/commit changes, however. Item must have name, user, importable, and slug attributes. """ - self.create_unique_slug( trans, item, flush=False ) + self.create_unique_slug( item, flush=False ) return self._session_setattr( item, 'importable', True, flush=flush ) - def make_non_importable( self, trans, item, flush=True ): + def make_non_importable( self, item, flush=True ): """ Makes item accessible--viewable and importable--and sets item's slug. Does not flush/commit changes, however. Item must have name, user, @@ -99,110 +95,112 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu """ # item must be unpublished if non-importable if item.published: - self.unpublish( trans, item, flush=False ) + self.unpublish( item, flush=False ) return self._session_setattr( item, 'importable', False, flush=flush ) # .... published - def publish( self, trans, item, flush=True ): + def publish( self, item, flush=True ): """ Set both the importable and published flags on `item` to True. """ # item must be importable to be published if not item.importable: - self.make_importable( trans, item, flush=False ) + self.make_importable( item, flush=False ) return self._session_setattr( item, 'published', True, flush=flush ) - def unpublish( self, trans, item, flush=True ): + def unpublish( self, item, flush=True ): """ Set the published flag on `item` to False. """ return self._session_setattr( item, 'published', False, flush=flush ) - #def _query_published( self, trans, filters=None, **kwargs ): - # """ - # """ - # published_filter = self.model_class.published == True - # filters = self._munge_filters( published_filter, filters ) - # return self.list( trans, filters=filters, **kwargs ) - # - #def list_published( self, trans, **kwargs ): - # """ - # """ - # query = self._query_published( trans, user, **kwargs ) - # return self.list( trans, query=query, **kwargs ) + def _query_published( self, filters=None, **kwargs ): + """ + Query all published items. + """ + published_filter = self.model_class.published == True + filters = self._munge_filters( published_filter, filters ) + return self.list( filters=filters, **kwargs ) + + def list_published( self, **kwargs ): + """ + Return a list of all published items. + """ + query = self._query_published( **kwargs ) + return self.list( query=query, **kwargs ) # .... user sharing # sharing is often done via a 3rd table btwn a User and an item -> a UserShareAssociation - def get_share_assocs( self, trans, item, user=None ): + def get_share_assocs( self, item, user=None ): """ Get the UserShareAssociations for the `item`. Optionally send in `user` to test for a single match. """ - query = self.query_associated( trans, self.user_share_model, item ) + query = self.query_associated( self.user_share_model, item ) if user is not None: query = query.filter_by( user=user ) return query.all() - def share_with( self, trans, item, user, flush=True ): + def share_with( self, item, user, flush=True ): """ Get or create a share for the given user (or users if `user` is a list). """ # allow user to be a list and call recursivly if isinstance( user, list ): - return map( lambda user: self.share_with( trans, item, user, flush=False ), user ) + return map( lambda user: self.share_with( item, user, flush=False ), user ) # get or create - existing = self.get_share_assocs( trans, item, user=user ) + existing = self.get_share_assocs( item, user=user ) if existing: return existing.pop( 0 ) - return self._create_user_share_assoc( trans, item, user, flush=flush ) + return self._create_user_share_assoc( item, user, flush=flush ) - def _create_user_share_assoc( self, trans, item, user, flush=True ): + def _create_user_share_assoc( self, item, user, flush=True ): """ Create a share for the given user. """ user_share_assoc = self.user_share_model() - self.app.model.context.add( user_share_assoc ) - self.associate( trans, user_share_assoc, item ) + self.session().add( user_share_assoc ) + self.associate( user_share_assoc, item ) user_share_assoc.user = user # ensure an item slug so shared users can access if not item.slug: - self.create_unique_slug( trans, item ) + self.create_unique_slug( item ) if flush: - self.app.model.context.flush() + self.session().flush() return user_share_assoc - def unshare_with( self, trans, item, user, flush=True ): + def unshare_with( self, item, user, flush=True ): """ Delete a user share (or list of shares) from the database. """ if isinstance( user, list ): - return map( lambda user: self.unshare_with( trans, item, user, flush=False ), user ) + return map( lambda user: self.unshare_with( item, user, flush=False ), user ) # Look for and delete sharing relation for user. - user_share_assoc = self.get_share_assocs( trans, item, user=user )[0] - self.app.model.context.delete( user_share_assoc ) + user_share_assoc = self.get_share_assocs( item, user=user )[0] + self.session().delete( user_share_assoc ) if flush: - self.app.model.context.flush() + self.session().flush() return user_share_assoc - #def _query_shared_with( self, trans, user, filters=None, **kwargs ): + #def _query_shared_with( self, user, filters=None, **kwargs ): ##TODO: # """ # """ # pass - #def list_shared_with( self, trans, user, **kwargs ): + #def list_shared_with( self, user, **kwargs ): # """ # """ - # query = self._query_shared_with( trans, user, **kwargs ) - # return self.list( trans, query=query, **kwargs ) + # query = self._query_shared_with( user, **kwargs ) + # return self.list( query=query, **kwargs ) # .... slugs # slugs are human readable strings often used to link to sharable resources (replacing ids) #TODO: as validator, deserializer, etc. (maybe another object entirely?) - def set_slug( self, trans, item, new_slug, flush=True ): + def set_slug( self, item, new_slug, user, flush=True ): """ Validate and set the new slug for `item`. """ @@ -211,12 +209,12 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu raise exceptions.RequestParameterInvalidException( "Invalid slug", slug=new_slug ) # error if slug is already in use - if self._slug_exists( trans, trans.user, new_slug ): + if self._slug_exists( user, new_slug ): raise exceptions.Conflict( "Slug already exists", slug=new_slug ) item.slug = new_slug if flush: - self.app.model.context.flush() + self.session().flush() return item def is_valid_slug( self, slug ): @@ -226,13 +224,13 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu VALID_SLUG_RE = re.compile( "^[a-z0-9\-]+$" ) return VALID_SLUG_RE.match( slug ) - def _existing_set_of_slugs( self, trans, user ): - query = ( self.app.model.context.query( self.model_class.slug ) + def _existing_set_of_slugs( self, user ): + query = ( self.session().query( self.model_class.slug ) .filter_by( user=user ) ) - return set( query.all() ) + return list( set( query.all() ) ) - def _slug_exists( self, trans, user, slug ): - query = ( self.app.model.context.query( self.model_class.slug ) + def _slug_exists( self, user, slug ): + query = ( self.session().query( self.model_class.slug ) .filter_by( user=user, slug=slug ) ) return query.count() != 0 @@ -246,13 +244,13 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu slug_base = slug_base[:-1] return slug_base - def _default_slug_base( self, trans, item ): + def _default_slug_base( self, item ): # override in subclasses if hasattr( item, 'title' ): return item.title.lower() return item.name.lower() - def get_unique_slug( self, trans, item ): + def get_unique_slug( self, item ): """ Returns a slug that is unique among user's importable items for item's class. @@ -261,7 +259,7 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu # Setup slug base. if cur_slug is None or cur_slug == "": - slug_base = self._slugify( self._default_slug_base( trans, item ) ) + slug_base = self._slugify( self._default_slug_base( item ) ) else: slug_base = cur_slug @@ -269,7 +267,7 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu # add integer to end. new_slug = slug_base count = 1 - while ( self.app.model.context.query( item.__class__ ) + while ( self.session().query( item.__class__ ) .filter_by( user=item.user, slug=new_slug, importable=True ) .count() != 0 ): # Slug taken; choose a new slug based on count. This approach can @@ -279,28 +277,28 @@ class SharableModelManager( base.ModelManager, secured.OwnableManagerMixin, secu return new_slug - def create_unique_slug( self, trans, item, flush=True ): + def create_unique_slug( self, item, flush=True ): """ Set a new, unique slug on the item. """ - item.slug = self.get_unique_slug( trans, item ) - self.app.model.context.add( item ) + item.slug = self.get_unique_slug( item ) + self.session().add( item ) if flush: - self.app.model.context.flush() + self.session().flush() return item - #def by_slug( self, trans, user, **kwargs ): + #def by_slug( self, user, **kwargs ): # """ # """ # pass # .... display - #def display_by_username_and_slug( self, trans, username, slug ): + #def display_by_username_and_slug( self, username, slug ): # """ Display item by username and slug. """ - #def set_public_username( self, trans, id, username, **kwargs ): + #def set_public_username( self, id, username, **kwargs ): # """ Set user's public username and delegate to sharing() """ - #def sharing( self, trans, id, **kwargs ): + #def sharing( self, id, **kwargs ): # """ Handle item sharing. """ @@ -319,20 +317,21 @@ class SharableModelSerializer( base.ModelSerializer, 'user_id' : self.serialize_id, 'username_and_slug' : self.serialize_username_and_slug }) + # these use the default serializer but must still be white-listed self.serializable_keyset.update([ 'importable', 'published', 'slug' ]) - def serialize_username_and_slug( self, trans, item, key ): + def serialize_username_and_slug( self, item, key, **context ): if not ( item.user and item.slug and self.SINGLE_CHAR_ABBR ): return None -#TODO: self.url_for return ( '/' ).join(( 'u', item.user.username, self.SINGLE_CHAR_ABBR, item.slug ) ) - #def published_url( self, trans, item, key ): + # def serialize_username_and_slug( self, item, key, **context ): # """ # """ - # url = url_for(controller='history', action="display_by_username_and_slug", + # #TODO: replace the above with this + # url = self.url_for(controller='history', action="display_by_username_and_slug", # username=item.user.username, slug=item.slug ) # return url @@ -356,7 +355,7 @@ class SharableModelDeserializer( base.ModelDeserializer, 'importable' : self.deserialize_importable, }) - def deserialize_published( self, trans, item, key, val ): + def deserialize_published( self, item, key, val, **context ): """ """ val = self.validate.bool( key, val ) @@ -364,12 +363,12 @@ class SharableModelDeserializer( base.ModelDeserializer, return val if val: - self.manager.publish( trans, item, flush=False ) + self.manager.publish( item, flush=False ) else: - self.manager.unpublish( trans, item, flush=False ) + self.manager.unpublish( item, flush=False ) return item.published - def deserialize_importable( self, trans, item, key, val ): + def deserialize_importable( self, item, key, val, **context ): """ """ val = self.validate.bool( key, val ) @@ -377,12 +376,12 @@ class SharableModelDeserializer( base.ModelDeserializer, return val if val: - self.manager.make_importable( trans, item, flush=False ) + self.manager.make_importable( item, flush=False ) else: - self.manager.make_non_importable( trans, item, flush=False ) + self.manager.make_non_importable( item, flush=False ) return item.published - #def deserialize_slug( self, trans, item, val ): + #def deserialize_slug( self, item, val, **context ): # """ # """ # #TODO: call manager.set_slug @@ -391,10 +390,15 @@ class SharableModelDeserializer( base.ModelDeserializer, #def deserialize_user_shares(): -class SharableModelFilters( base.ModelFilterParser ): +class SharableModelFilters( base.ModelFilterParser, + taggable.TaggableFilterMixin, + annotatable.AnnotatableFilterMixin ): def _add_parsers( self ): super( SharableModelFilters, self )._add_parsers() + taggable.TaggableFilterMixin._add_parsers( self ) + annotatable.AnnotatableFilterMixin._add_parsers( self ) + self.orm_filter_parsers.update({ 'importable' : { 'op': ( 'eq' ), 'val': self.parse_bool }, 'published' : { 'op': ( 'eq' ), 'val': self.parse_bool }, diff --git a/lib/galaxy/managers/taggable.py b/lib/galaxy/managers/taggable.py index e0ec6427579..7ecfe971304 100644 --- a/lib/galaxy/managers/taggable.py +++ b/lib/galaxy/managers/taggable.py @@ -15,7 +15,7 @@ class TaggableManagerMixin( object ): #TODO: most of this can be done by delegating to the TagManager? - #def by_user( self, trans, user, **kwargs ): + #def by_user( self, user, **kwargs ): # pass @@ -24,7 +24,7 @@ class TaggableSerializerMixin( object ): def add_serializers( self ): self.serializers[ 'tags' ] = self.serialize_tags - def serialize_tags( self, trans, item, key ): + def serialize_tags( self, item, key, **context ): """ Return tags as a list of strings. """ @@ -45,7 +45,7 @@ class TaggableDeserializerMixin( object ): def add_deserializers( self ): self.deserializers[ 'tags' ] = self.deserialize_tags - def deserialize_tags( self, trans, item, key, val ): + def deserialize_tags( self, item, key, val, user=None, **context ): """ Make sure `val` is a valid list of tag strings and assign them. @@ -53,8 +53,6 @@ class TaggableDeserializerMixin( object ): """ new_tags_list = self.validate.basestring_list( key, val ) #TODO: have to assume trans.user here... - #TODO: trans - user = trans.user #TODO: duped from tags manager - de-dupe when moved to taggable mixin tag_handler = self.app.tag_handler tag_handler.delete_item_tags( user, item ) @@ -64,3 +62,43 @@ class TaggableDeserializerMixin( object ): #TODO:!! does the creation of new_tags_list mean there are now more and more unused tag rows in the db? return item.tags +class TaggableFilterMixin( object ): + + def _tag_str_gen( self, item ): + """ + Return a list of strings built from the item's tags. + """ + #TODO: which user is this? all? + for tag in item.tags: + tag_str = tag.user_tname + if tag.value is not None: + tag_str += ":" + tag.user_value + yield tag_str + + def filter_has_partial_tag( self, item, val ): + """ + Return True if any tag partially contains `val`. + """ + for tag_str in self._tag_str_gen( item ): + if val in tag_str: + return True + return False + + def filter_has_tag( self, item, val ): + """ + Return True if any tag exactly equals `val`. + """ + for tag_str in self._tag_str_gen( item ): + if val == tag_str: + return True + return False + + def _add_parsers( self ): + self.fn_filter_parsers.update({ + 'tag': { + 'op': { + 'eq' : self.filter_has_tag, + 'has' : self.filter_has_partial_tag, + } + } + }) diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py index 3f581b2558f..4b4a8fcf003 100644 --- a/lib/galaxy/managers/users.py +++ b/lib/galaxy/managers/users.py @@ -31,7 +31,7 @@ class UserManager( base.ModelManager, deletable.PurgableManagerMixin ): #TODO: incorporate security/validate_user_input.py #TODO: incorporate UsesFormDefinitionsMixin? - def create( self, trans, **kwargs ): + def create( self, webapp_name=None, **kwargs ): """ Create a new user. """ @@ -39,7 +39,7 @@ class UserManager( base.ModelManager, deletable.PurgableManagerMixin ): email = kwargs[ 'email' ] username = kwargs[ 'username' ] password = kwargs[ 'password' ] - self._error_on_duplicate_email( trans, email ) + self._error_on_duplicate_email( email ) user = model.User( email=email, password=password ) user.username = username @@ -50,9 +50,9 @@ class UserManager( base.ModelManager, deletable.PurgableManagerMixin ): # Activation is off, every new user is active by default. user.active = True - self.app.model.context.add( user ) + self.session().add( user ) try: - self.app.model.context.flush() + self.session().flush() #TODO:?? flush needed for permissions below? If not, make optional except sqlalchemy.exc.IntegrityError, db_err: raise exceptions.Conflict( db_err.message ) @@ -60,72 +60,71 @@ class UserManager( base.ModelManager, deletable.PurgableManagerMixin ): # can throw an sqlalx.IntegrityError if username not unique self.app.security_agent.create_private_user_role( user ) - #TODO: trans - if trans.webapp.name == 'galaxy': + if webapp_name == 'galaxy': # We set default user permissions, before we log in and set the default history permissions permissions = self.app.config.new_user_dataset_access_role_default_private self.app.security_agent.user_set_default_permissions( user, default_access_private=permissions ) return user - def _error_on_duplicate_email( self, trans, email ): + def _error_on_duplicate_email( self, email ): """ Check for a duplicate email and raise if found. :raises exceptions.Conflict: if any are found """ #TODO: remove this check when unique=True is added to the email column - if self.by_email( trans, email ) is not None: + if self.by_email( email ) is not None: raise exceptions.Conflict( 'Email must be unique', email=email ) # ---- filters - def by_email( self, trans, email, filters=None, **kwargs ): + def by_email( self, email, filters=None, **kwargs ): """ Find a user by their email. """ filters = self._munge_filters( self.model_class.email == email, filters ) try: #TODO: use one_or_none - return super( UserManager, self ).one( trans, filters=filters, **kwargs ) + return super( UserManager, self ).one( filters=filters, **kwargs ) except exceptions.ObjectNotFound, not_found: return None - def by_email_like( self, trans, email_with_wildcards, filters=None, order_by=None, **kwargs ): + def by_email_like( self, email_with_wildcards, filters=None, order_by=None, **kwargs ): """ Find a user searching with SQL wildcards. """ filters = self._munge_filters( self.model_class.email.like( email_with_wildcards ), filters ) order_by = order_by or ( model.User.email, ) - return super( UserManager, self ).list( trans, filters=filters, order_by=order_by, **kwargs ) + return super( UserManager, self ).list( filters=filters, order_by=order_by, **kwargs ) # ---- admin - def is_admin( self, trans, user ): + def is_admin( self, user ): """ Return True if this user is an admin. """ - admin_emails = self._admin_emails( trans ) + admin_emails = self._admin_emails() return user and admin_emails and user.email in admin_emails - def _admin_emails( self, trans ): + def _admin_emails( self ): """ Return a list of admin email addresses from the config file. """ return [ email.strip() for email in self.app.config.get( "admin_users", "" ).split( "," ) ] - def admins( self, trans, filters=None, **kwargs ): + def admins( self, filters=None, **kwargs ): """ Return a list of admin Users. """ - filters = self._munge_filters( self.model_class.email.in_( self._admin_emails( trans ) ), filters ) - return super( UserManager, self ).list( trans, filters=filters, **kwargs ) + filters = self._munge_filters( self.model_class.email.in_( self._admin_emails() ), filters ) + return super( UserManager, self ).list( filters=filters, **kwargs ) - def error_unless_admin( self, trans, user, msg="Administrators only", **kwargs ): + def error_unless_admin( self, user, msg="Administrators only", **kwargs ): """ Raise an error if `user` is not an admin. :raises exceptions.AdminRequiredException: if `user` is not an admin. """ # useful in admin only methods - if not self.is_admin( trans, user ): + if not self.is_admin( user ): raise exceptions.AdminRequiredException( msg, **kwargs ) return user @@ -137,22 +136,23 @@ class UserManager( base.ModelManager, deletable.PurgableManagerMixin ): # define here for single point of change and make more readable return user is None - def error_if_anonymous( self, trans, user, msg="Log-in required", **kwargs ): + def error_if_anonymous( self, user, msg="Log-in required", **kwargs ): """ Raise an error if `user` is anonymous. """ if user is None: - #TODO: code is correct (403), but should be named AuthenticationRequired (401 and 403 are flipped) + #TODO: code is correct (401) but should be named AuthenticationRequired (401 and 403 are flipped) raise exceptions.AuthenticationFailed( msg, **kwargs ) return user # ---- current def current_user( self, trans ): # define here for single point of change and make more readable + # TODO: trans return trans.user # ---- api keys - def create_api_key( self, trans, user ): + def create_api_key( self, user ): """ Create and return an API key for `user`. """ @@ -160,46 +160,48 @@ class UserManager( base.ModelManager, deletable.PurgableManagerMixin ): return api_keys.ApiKeyManager( self.app ).create_api_key( user ) #TODO: possibly move to ApiKeyManager - def valid_api_key( self, trans, user ): + def valid_api_key( self, user ): """ Return this most recent APIKey for this user or None if none have been created. """ - query = ( self.app.model.context.query( model.APIKeys ) + query = ( self.session().query( model.APIKeys ) .filter_by( user=user ) .order_by( sqlalchemy.desc( model.APIKeys.create_time ) ) ) all = query.all() - for a in all: - print a.user.username, a.key, a.create_time - print all if len( all ): return all[0] return None #return query.first() #TODO: possibly move to ApiKeyManager - def get_or_create_valid_api_key( self, trans, user ): + def get_or_create_valid_api_key( self, user ): """ Return this most recent APIKey for this user or create one if none have been created. """ - existing = self.valid_api_key( trans, user ) + existing = self.valid_api_key( user ) if existing: return existing - return self.create_api_key( self, trans, user ) + return self.create_api_key( self, user ) - # ---- roles - def private_role( self, trans, user ): - """ - Return the security agent's private role for `user`. - """ - #TODO: not sure we need to go through sec agent... it's just the first role of type private + # ---- preferences + + + # ---- roles and permissions + def private_role( self, user ): return self.app.security_agent.get_private_user_role( user ) - def quota( self, trans, user ): + def sharing_roles( self, user ): + return self.app.security_agent.get_sharing_roles( user ) + + def default_permissions( self, user ): + return self.app.security_agent.user_get_default_permissions( user ) + + def quota( self, user ): #TODO: use quota manager return self.app.quota_agent.get_percent( user=user ) - def tags_used( self, trans, user, tag_models=None ): + def tags_used( self, user, tag_models=None ): """ Return a list of distinct 'user_tname:user_value' strings that the given user has used. @@ -214,8 +216,8 @@ class UserManager( base.ModelManager, deletable.PurgableManagerMixin ): # create a union of subqueries for each for this user - getting only the tname and user_value all_tags_query = None for tag_model in tag_models: - subq = ( self.app.model.context.query( tag_model.user_tname, tag_model.user_value ) - .filter( tag_model.user == trans.user ) ) + subq = ( self.session().query( tag_model.user_tname, tag_model.user_value ) + .filter( tag_model.user == user ) ) all_tags_query = subq if all_tags_query is None else all_tags_query.union( subq ) # if nothing init'd the query, bail @@ -227,7 +229,7 @@ class UserManager( base.ModelManager, deletable.PurgableManagerMixin ): tags = [( ( name + ':' + val ) if val else name ) for name, val in tags ] return sorted( tags ) - def has_requests( self, trans, user ): + def has_requests( self, user, trans ): """ """ if self.is_anonymous( user ): @@ -255,7 +257,7 @@ class UserSerializer( base.ModelSerializer, deletable.PurgableSerializerMixin ): 'total_disk_usage', 'nice_total_disk_usage', - 'quota_percent' + 'quota_percent', #'deleted', #'purged', @@ -276,30 +278,43 @@ class UserSerializer( base.ModelSerializer, deletable.PurgableSerializerMixin ): 'id' : self.serialize_id, 'create_time' : self.serialize_date, 'update_time' : self.serialize_date, - 'is_admin' : lambda t, i, k: self.user_manager.is_admin( t, i ), + 'is_admin' : lambda i, k, **c: self.user_manager.is_admin( i ), - 'total_disk_usage' : lambda t, i, k: float( i.total_disk_usage ), - 'quota_percent' : lambda t, i, k: self.user_manager.quota( t, i ), + 'total_disk_usage' : lambda i, k, **c: float( i.total_disk_usage ), + 'quota_percent' : lambda i, k, **c: self.user_manager.quota( i ), - 'tags_used' : lambda t, i, k: self.user_manager.tags_used( t, i ), + 'tags_used' : lambda i, k, **c: self.user_manager.tags_used( i ), #TODO: 'has_requests' is more apt - 'requests' : lambda t, i, k: self.user_manager.has_requests( t, i ) + 'requests' : lambda i, k, trans=None, **c: self.user_manager.has_requests( i, trans ) }) - def serialize_current_anonymous_user( self, trans, user, keys ): + +class CurrentUserSerializer( UserSerializer ): + + def serialize( self, user, keys, **kwargs ): + """ + Override to return at least some usage info if user is anonymous. + """ +# hmmm. + kwargs[ 'current_user' ] = user + if self.user_manager.is_anonymous( user ): + return self.serialize_current_anonymous_user( user, keys, **kwargs ) + return super( UserSerializer, self ).serialize( user, keys, **kwargs ) + + def serialize_current_anonymous_user( self, user, keys, trans=None, **kwargs ): # use the current history if any to get usage stats for trans' anonymous user #TODO: might be better as sep. Serializer class history = trans.history if not history: raise exceptions.AuthenticationRequired( 'No history for anonymous user usage stats' ); - usage = trans.app.quota_agent.get_usage( trans, history=trans.history ) - percent = trans.app.quota_agent.get_percent( trans=trans, usage=usage ) + usage = self.app.quota_agent.get_usage( trans, history=trans.history ) + percent = self.app.quota_agent.get_percent( trans=trans, usage=usage ) # a very small subset of keys available values = { 'id' : None, - 'total_disk_usage' : int( usage ), + 'total_disk_usage' : float( usage ), 'nice_total_disk_usage' : util.nice_size( usage ), 'quota_percent' : percent, } @@ -309,25 +324,16 @@ class UserSerializer( base.ModelSerializer, deletable.PurgableSerializerMixin ): serialized[ key ] = values[ key ] return serialized - def serialize( self, trans, user, keys ): - """ - Override to return at least some usage info if user is anonymous. - """ - if self.user_manager.is_anonymous( user ): - return self.serialize_current_anonymous_user( trans, user, keys ) - return super( UserSerializer, self ).serialize( trans, user, keys ) - -class AdminUserFilters( base.ModelFilterParser, deletable.PurgableFiltersMixin ): +class AdminUserFilterParser( base.ModelFilterParser, deletable.PurgableFiltersMixin ): model_class = model.User def _add_parsers( self ): - super( AdminUserFilters, self )._add_parsers() + super( AdminUserFilterParser, self )._add_parsers() deletable.PurgableFiltersMixin._add_parsers( self ) #PRECONDITION: user making the query has been verified as an admin self.orm_filter_parsers.update({ - 'name' : { 'op': ( 'eq', 'contains', 'like' ) }, 'email' : { 'op': ( 'eq', 'contains', 'like' ) }, 'username' : { 'op': ( 'eq', 'contains', 'like' ) }, 'active' : { 'op': ( 'eq' ) }, diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 50041ff4f69..55ab8c1019e 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -103,7 +103,6 @@ model.HistoryDatasetAssociation.table = Table( "history_dataset_association", me Column( "state", TrimmedString( 64 ), index=True, key="_state" ), Column( "copied_from_history_dataset_association_id", Integer, ForeignKey( "history_dataset_association.id" ), nullable=True ), Column( "copied_from_library_dataset_dataset_association_id", Integer, ForeignKey( "library_dataset_dataset_association.id" ), nullable=True ), - Column( "hid", Integer ), Column( "name", TrimmedString( 255 ) ), Column( "info", TrimmedString( 255 ) ), Column( "blurb", TrimmedString( 255 ) ), @@ -114,12 +113,14 @@ model.HistoryDatasetAssociation.table = Table( "history_dataset_association", me Column( "parent_id", Integer, ForeignKey( "history_dataset_association.id" ), nullable=True ), Column( "designation", TrimmedString( 255 ) ), Column( "deleted", Boolean, index=True, default=False ), - Column( "purged", Boolean, index=True, default=False ), Column( "visible", Boolean ), + Column( "extended_metadata_id", Integer, ForeignKey( "extended_metadata.id" ), index=True ), + # differs from ldda + Column( "hid", Integer ), + Column( "purged", Boolean, index=True, default=False ), Column( "hidden_beneath_collection_instance_id", ForeignKey( "history_dataset_collection_association.id" ), nullable=True ), - Column( "extended_metadata_id", Integer, - ForeignKey( "extended_metadata.id" ), index=True ) - ) +) + model.Dataset.table = Table( "dataset", metadata, Column( "id", Integer, primary_key=True ), @@ -322,11 +323,11 @@ model.LibraryDatasetDatasetAssociation.table = Table( "library_dataset_dataset_a Column( "designation", TrimmedString( 255 ) ), Column( "deleted", Boolean, index=True, default=False ), Column( "visible", Boolean ), + Column( "extended_metadata_id", Integer, ForeignKey( "extended_metadata.id" ), index=True ), + # differs from hda Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), Column( "message", TrimmedString( 255 ) ), - Column( "extended_metadata_id", Integer, - ForeignKey( "extended_metadata.id" ), index=True ) - ) +) model.ExtendedMetadata.table = Table("extended_metadata", metadata, diff --git a/lib/galaxy/security/validate_user_input.py b/lib/galaxy/security/validate_user_input.py index 0c15cacdc01..59cbb6a5640 100644 --- a/lib/galaxy/security/validate_user_input.py +++ b/lib/galaxy/security/validate_user_input.py @@ -37,7 +37,7 @@ def validate_email( trans, email, user=None, check_dup=True ): def validate_publicname( trans, publicname, user=None ): - # User names must be at least four characters in length and contain only lower-case + # User names must be at least three characters in length and contain only lower-case # letters, numbers, and the '-' character. if user and user.username == publicname: return '' @@ -45,8 +45,10 @@ def validate_publicname( trans, publicname, user=None ): if len( publicname ) < 3: return "Public name must be at least 3 characters in length" else: - if len( publicname ) < 4: - return "Public name must be at least 4 characters in length" + # DCT - TODO - Simplify logic if 3 chars is okay for publicname for + # galaxy as well as toolshed + if len( publicname ) < 3: + return "Public name must be at least 3 characters in length" if len( publicname ) > 255: return "Public name cannot be more than 255 characters in length" if not( VALID_PUBLICNAME_RE.match( publicname ) ): diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py index 7302242c24d..473910129ed 100755 --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -40,7 +40,7 @@ from galaxy.tools.parameters.basic import (BaseURLToolParameter, DataToolParameter, DataCollectionToolParameter, HiddenToolParameter, LibraryDatasetToolParameter, SelectToolParameter, ToolParameter, UnvalidatedValue, IntegerToolParameter, FloatToolParameter) -from galaxy.tools.parameters.grouping import Conditional, ConditionalWhen, Repeat, UploadDataset +from galaxy.tools.parameters.grouping import Conditional, ConditionalWhen, Repeat, Section, UploadDataset from galaxy.tools.parameters.input_translation import ToolInputTranslator from galaxy.tools.parameters.output import ToolOutputActionGroup from galaxy.tools.parameters.validation import LateValidationError @@ -972,6 +972,15 @@ class Tool( object, Dictifiable ): case.inputs = odict() group.cases.append( case ) rval[group.name] = group + elif input_type == "section": + group = Section() + group.name = input_source.get( "name" ) + group.title = input_source.get( "title" ) + group.help = input_source.get( "help", None ) + group.expanded = input_source.get_bool( "expanded", False ) + page_source = input_source.parse_nested_inputs_source() + group.inputs = self.parse_input_elem( page_source, enctypes, context ) + rval[group.name] = group elif input_type == "upload_dataset": elem = input_source.elem() group = UploadDataset() @@ -1505,6 +1514,20 @@ class Tool( object, Dictifiable ): group_state['__current_case__'] = current_case # Store the value of the test element group_state[ input.test_param.name ] = value + elif isinstance( input, Section ): + group_state = state[input.name] + group_prefix = "%s|" % ( key ) + self.fill_in_new_state( trans, input.inputs, group_state, context, history=history ) + group_errors = self.populate_state( trans, + input.inputs, + group_state, + incoming, + history, + source, + prefix=group_prefix, + context=context ) + if group_errors: + errors[ input.name ] = group_errors elif isinstance( input, UploadDataset ): group_state = state[input.name] group_errors = [] @@ -1714,6 +1737,22 @@ class Tool( object, Dictifiable ): group_state['__current_case__'] = current_case # Store the value of the test element group_state[ input.test_param.name ] = value + elif isinstance( input, Section ): + group_state = state[input.name] + group_old_errors = old_errors.get( input.name, {} ) + group_prefix = "%s|" % ( key ) + group_errors = self.update_state( trans, + input.inputs, + group_state, + incoming, + prefix=group_prefix, + context=context, + source=source, + update_only=update_only, + old_errors=group_old_errors, + item_callback=item_callback ) + if group_errors: + errors[ input.name ] = group_errors elif isinstance( input, UploadDataset ): group_state = state[input.name] group_errors = [] @@ -2383,6 +2422,10 @@ class Tool( object, Dictifiable ): errors[test_param_key] = 'The selected case is unavailable/invalid.' pass group_state[input.test_param.name] = value + elif input.type == 'section': + group_state = state[input.name] + group_prefix = "%s|" % ( key ) + populate_state(trans, input.inputs, group_state, errors, incoming, prefix=group_prefix, context=context) else: default_value = incoming.get(key, state.get(input.name, None)) value, error = check_state(trans, input, default_value, context) @@ -2417,11 +2460,13 @@ class Tool( object, Dictifiable ): if i == group_state.get('__current_case__', None): current_state = group_state iterate(tool_dict['cases'][i]['inputs'], input.cases[i].inputs, current_state, other_values) + elif input.type == 'section': + iterate( tool_dict['inputs'], input.inputs, group_state, other_values ) else: # identify name input_name = tool_dict.get('name') - # create expanded input dictionary incl. repeats and dynamic_parameters + # expand input dictionary incl. repeats and dynamic_parameters try: tool_dict = input.to_dict(trans, other_values=other_values) except Exception: @@ -2555,28 +2600,44 @@ class Tool( object, Dictifiable ): # dataset used; parameter should be the analygous dataset in the # current history. history = trans.get_history() - hda_source_dict = {} # Mapping from HDA in history to source HDAs. + + # Create index for hdas. + hda_source_dict = {} for hda in history.datasets: - source_hda = hda.copied_from_history_dataset_association - while source_hda: - if source_hda.dataset.id not in hda_source_dict or source_hda.hid == hda.hid: - hda_source_dict[ source_hda.dataset.id ] = hda - source_hda = source_hda.copied_from_history_dataset_association + key = '%s_%s' % (hda.hid, hda.dataset.id) + hda_source_dict[ hda.dataset.id ] = hda_source_dict[ key ] = hda # Ditto for dataset collections. hdca_source_dict = {} for hdca in history.dataset_collections: - source_hdca = hdca.copied_from_history_dataset_collection_association - while source_hdca: - if source_hdca.collection.id not in hdca_source_dict or source_hdca.hid == hdca.hid: - hdca_source_dict[ source_hdca.collection.id ] = hdca - source_hdca = source_hdca.copied_from_history_dataset_collection_association + key = '%s_%s' % (hdca.hid, hdca.collection.id) + hdca_source_dict[ hda.collection.id ] = hdca_source_dict[ key ] = hdca + + # Map dataset or collection to current history + def map_to_history(value): + id = None + source = None + if isinstance(value, trans.app.model.HistoryDatasetAssociation): + id = value.dataset.id + source = hda_source_dict + elif isinstance(value, trans.app.model.HistoryDatasetCollectionAssociation): + id = value.collection.id + source = hdca_source_dict + else: + return None + key = '%s_%s' % (value.hid, id) + if key in source: + return source[ key ] + elif id in source: + return source[ id ] + else: + return None # Unpack unvalidated values to strings, they'll be validated when the # form is submitted (this happens when re-running a job that was # initially run by a workflow) #This needs to be done recursively through grouping parameters - def rerun_callback( input, value, prefixed_name, prefixed_label ): + def mapping_callback( input, value, prefixed_name, prefixed_label ): if isinstance( value, UnvalidatedValue ): try: return input.to_html_value( value.value, trans.app ) @@ -2588,22 +2649,17 @@ class Tool( object, Dictifiable ): if isinstance(value,list): values = [] for val in value: - if isinstance(val, trans.app.model.HistoryDatasetAssociation): - if val.dataset.id in hda_source_dict: - values.append( hda_source_dict[ val.dataset.id ] ) - else: - values.append( val ) + new_val = map_to_history( val ) + if new_val: + values.append( new_val ) + else: + values.append( val ) return values - if isinstance(value, trans.app.model.HistoryDatasetAssociation): - if value.dataset.id in hda_source_dict: - return hda_source_dict[ value.dataset.id ] - if isinstance(value, trans.app.model.HistoryDatasetCollectionAssociation): - if value.collection.id in hdca_source_dict: - return hdca_source_dict[ value.collection.id ] + else: + return map_to_history( value ) elif isinstance( input, DataCollectionToolParameter ): - if value.collection.id in hdca_source_dict: - return hdca_source_dict[ value.collection.id ] - visit_input_values( tool_inputs, params, rerun_callback ) + return map_to_history( value ) + visit_input_values( tool_inputs, params, mapping_callback ) def _compare_tool_version( self, trans, job ): """ diff --git a/lib/galaxy/tools/evaluation.py b/lib/galaxy/tools/evaluation.py index bfe7fecf37d..05760d60a76 100644 --- a/lib/galaxy/tools/evaluation.py +++ b/lib/galaxy/tools/evaluation.py @@ -22,7 +22,7 @@ from galaxy.tools.parameters.basic import ( LibraryDatasetToolParameter, SelectToolParameter, ) -from galaxy.tools.parameters.grouping import Conditional, Repeat +from galaxy.tools.parameters.grouping import Conditional, Repeat, Section from galaxy.jobs.datasets import dataset_path_rewrites import logging @@ -143,6 +143,9 @@ class ToolEvaluator( object ): values = input_values[ input.name ] current = values["__current_case__"] do_walk( input.cases[current].inputs, values ) + elif isinstance( input, Section ): + values = input_values[ input.name ] + do_walk( input.inputs, values ) else: func( input_values, input ) diff --git a/lib/galaxy/tools/parameters/__init__.py b/lib/galaxy/tools/parameters/__init__.py index ff05dc9435d..b6c506057ae 100644 --- a/lib/galaxy/tools/parameters/__init__.py +++ b/lib/galaxy/tools/parameters/__init__.py @@ -33,6 +33,11 @@ def visit_input_values( inputs, input_values, callback, name_prefix="", label_pr label_prefix = label_prefix new_name_prefix = name_prefix + input.name + "|" visit_input_values( input.cases[current].inputs, values, callback, new_name_prefix, label_prefix ) + elif isinstance( input, Section ): + values = input_values[ input.name ] + label_prefix = label_prefix + new_name_prefix = name_prefix + input.name + "|" + visit_input_values( input.inputs, values, callback, new_name_prefix, label_prefix ) else: new_value = callback( input, input_values[input.name], @@ -122,6 +127,10 @@ def params_to_incoming( incoming, inputs, input_values, app, name_prefix="", to_ new_name_prefix = name_prefix + input.name + "|" incoming[ new_name_prefix + input.test_param.name ] = values[ input.test_param.name ] params_to_incoming( incoming, input.cases[current].inputs, values, app, new_name_prefix, to_html=to_html ) + elif isinstance( input, Section ): + values = input_values[ input.name ] + new_name_prefix = name_prefix + input.name + "|" + params_to_incoming( incoming, input.inputs, values, app, new_name_prefix, to_html=to_html ) else: value = input_values.get( input.name ) if to_html: diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index a075d4c72f5..31ee1b4152f 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -38,14 +38,15 @@ class ToolParameter( object, Dictifiable ): moment but in the future should encapsulate more complex parameters (lists of valid choices, validation logic, ...) """ - dict_collection_visible_keys = ( 'name', 'type', 'label', 'help' ) + dict_collection_visible_keys = ( 'name', 'argument', 'type', 'label', 'help' ) def __init__( self, tool, input_source, context=None ): input_source = ensure_input_source(input_source) self.tool = tool self.refresh_on_change = False self.refresh_on_change_values = [] - self.name = input_source.get("name") + self.argument = input_source.get("argument") + self.name = ToolParameter.parse_name( input_source ) self.type = input_source.get("type") self.hidden = input_source.get("hidden", False) self.is_dynamic = False @@ -222,9 +223,7 @@ class ToolParameter( object, Dictifiable ): @classmethod def build( cls, tool, param ): """Factory method to create parameter of correct type""" - param_name = param.get( "name" ) - if not param_name: - raise ValueError( "Tool parameter '%s' requires a 'name'" % (param_name ) ) + param_name = cls.parse_name( param ) param_type = param.get("type") if not param_type: raise ValueError( "Tool parameter '%s' requires a 'type'" % ( param_name ) ) @@ -233,6 +232,17 @@ class ToolParameter( object, Dictifiable ): else: return parameter_types[param_type]( tool, param ) + @classmethod + def parse_name(cls, input_source): + name = input_source.get("name", None) + if name is None: + argument = input_source.get("argument") + if argument: + name = argument.lstrip("-") + else: + raise ValueError("Tool parameter must specify a name.") + return name + class TextToolParameter( ToolParameter ): """ @@ -813,7 +823,8 @@ class SelectToolParameter( ToolParameter ): call_other_values = self._get_dynamic_options_call_other_values( trans, other_values ) try: return eval( self.dynamic_options, self.tool.code_namespace, call_other_values ) - except Exception: + except Exception, e: + log.debug( "Error determining dynamic options for parameter '%s' in tool '%s':", self.name, self.tool.id, exc_info=e ) return [] else: return self.static_options diff --git a/lib/galaxy/tools/parameters/grouping.py b/lib/galaxy/tools/parameters/grouping.py index 30b9dfa6c4c..000b4d72eeb 100644 --- a/lib/galaxy/tools/parameters/grouping.py +++ b/lib/galaxy/tools/parameters/grouping.py @@ -129,6 +129,55 @@ class Repeat( Group ): repeat_dict[ "inputs" ] = map( input_to_dict, self.inputs.values() ) return repeat_dict +class Section( Group ): + + dict_collection_visible_keys = ( 'name', 'type', 'title', 'help', 'expanded') + + type = "section" + def __init__( self ): + Group.__init__( self ) + self.title = None + self.inputs = None + self.help = None + self.expanded = False + @property + def title_plural( self ): + return inflector.pluralize( self.title ) + def label( self ): + return "Section (%s)" % self.title + def value_to_basic( self, value, app ): + rval = {} + for input in self.inputs.itervalues(): + rval[ input.name ] = input.value_to_basic( value[input.name], app ) + return rval + def value_from_basic( self, value, app, ignore_errors=False ): + rval = {} + try: + for input in self.inputs.itervalues(): + if not ignore_errors or input.name in value: + rval[ input.name ] = input.value_from_basic( value[ input.name ], app, ignore_errors ) + except Exception, e: + if not ignore_errors: + raise e + return rval + def visit_inputs( self, prefix, value, callback ): + for input in self.inputs.itervalues(): + if isinstance( input, ToolParameter ): + callback( prefix, input, value[input.name], parent = value ) + else: + input.visit_inputs( prefix, value[input.name], callback ) + def get_initial_value( self, trans, context, history=None ): + rval = {} + child_context = ExpressionContext( rval, context ) + for child_input in self.inputs.itervalues(): + rval[ child_input.name ] = child_input.get_initial_value( trans, child_context, history=history ) + return rval + def to_dict( self, trans, view='collection', value_mapper=None ): + section_dict = super( Section, self ).to_dict( trans, view=view, value_mapper=value_mapper ) + def input_to_dict( input ): + return input.to_dict( trans, view=view, value_mapper=value_mapper ) + section_dict[ "inputs" ] = map( input_to_dict, self.inputs.values() ) + return section_dict class UploadDataset( Group ): type = "upload_dataset" diff --git a/lib/galaxy/tools/parameters/wrapped.py b/lib/galaxy/tools/parameters/wrapped.py index 473799b5076..ea610b06976 100644 --- a/lib/galaxy/tools/parameters/wrapped.py +++ b/lib/galaxy/tools/parameters/wrapped.py @@ -8,6 +8,7 @@ from galaxy.tools.parameters.basic import ( from galaxy.tools.parameters.grouping import ( Repeat, Conditional, + Section ) PARAMS_UNWRAPPED = object() @@ -44,6 +45,9 @@ class WrappedParameters( object ): values = input_values[ input.name ] current = values[ "__current_case__" ] self.wrap_values( input.cases[current].inputs, values, skip_missing_values=skip_missing_values ) + elif isinstance( input, Section ): + values = input_values[ input.name ] + self.wrap_values( input.inputs, values, skip_missing_values=skip_missing_values ) elif isinstance( input, DataToolParameter ) and input.multiple: input_values[ input.name ] = \ galaxy.tools.DatasetListWrapper( input_values[ input.name ], diff --git a/lib/galaxy/tools/parser/xml.py b/lib/galaxy/tools/parser/xml.py index e2bbb609bcf..d28b0076e3f 100644 --- a/lib/galaxy/tools/parser/xml.py +++ b/lib/galaxy/tools/parser/xml.py @@ -422,6 +422,13 @@ def __expand_input_elems( root_elem, prefix="" ): __pull_up_params( root_elem, cond_elem ) root_elem.remove( cond_elem ) + section_elems = root_elem.findall( 'section' ) + for section_elem in section_elems: + new_prefix = __prefix_join( prefix, section_elem.get( "name" ) ) + __expand_input_elems( section_elem, new_prefix ) + __pull_up_params( root_elem, section_elem ) + root_elem.remove( section_elem ) + def __append_prefix_to_params( elem, prefix ): for param_elem in elem.findall( 'param' ): diff --git a/lib/galaxy/tools/test.py b/lib/galaxy/tools/test.py index 8cfe5bcd846..03f48106e6a 100644 --- a/lib/galaxy/tools/test.py +++ b/lib/galaxy/tools/test.py @@ -166,7 +166,12 @@ class ToolTestBuilder( object ): # an infinite loop - hence the "case_value is not None" # check. expanded_inputs[ case_context.for_state() ] = expanded_case_value - + elif isinstance( value, galaxy.tools.parameters.grouping.Section ): + context = ParamContext( name=value.name, parent_context=parent_context ) + for r_name, r_value in value.inputs.iteritems(): + expanded_input = self.__process_raw_inputs( { context.for_state(): r_value }, raw_inputs, parent_context=context ) + if expanded_input: + expanded_inputs.update( expanded_input ) elif isinstance( value, galaxy.tools.parameters.grouping.Repeat ): repeat_index = 0 while True: diff --git a/lib/galaxy/util/dbkeys.py b/lib/galaxy/util/dbkeys.py index 9bf7d09e8e3..c537d6999ec 100644 --- a/lib/galaxy/util/dbkeys.py +++ b/lib/galaxy/util/dbkeys.py @@ -38,7 +38,7 @@ class GenomeBuilds( object ): for dataset in datasets: rval.append( ( dataset.dbkey, "%s (%s) [History]" % ( dataset.name, dataset.dbkey ) ) ) user = trans.user - if user and 'dbkeys' in user.preferences: + if user and hasattr( user, 'preferences' ) and 'dbkeys' in user.preferences: user_keys = loads( user.preferences['dbkeys'] ) for key, chrom_dict in user_keys.iteritems(): rval.append( ( key, "%s (%s) [Custom]" % ( chrom_dict['name'], key ) ) ) diff --git a/lib/galaxy/visualization/registry.py b/lib/galaxy/visualization/registry.py index d94841aa4ef..053862a4137 100644 --- a/lib/galaxy/visualization/registry.py +++ b/lib/galaxy/visualization/registry.py @@ -961,7 +961,7 @@ class ResourceParser( object ): elif param_type == 'dataset': decoded_dataset_id = trans.security.decode_id( query_param ) - parsed_param = controller.hda_manager.get_accessible( trans, decoded_dataset_id, trans.user ) + parsed_param = controller.hda_manager.get_accessible( decoded_dataset_id, trans.user ) elif param_type == 'hda_or_ldda': encoded_dataset_id = query_param diff --git a/lib/galaxy/web/base/controllers/admin.py b/lib/galaxy/web/base/controllers/admin.py index e4ca72649dc..349d83df3a5 100644 --- a/lib/galaxy/web/base/controllers/admin.py +++ b/lib/galaxy/web/base/controllers/admin.py @@ -1039,7 +1039,7 @@ class Admin( object ): stop_msg += '.' for job_id in job_ids: error_msg = "This job was stopped by an administrator: %s Contact support for additional help." \ - % ( stop_msg, self.app.config.get("support_url", "http://wiki.galaxyproject.org/Support" ) ) + % ( stop_msg, self.app.config.get("support_url", "https://wiki.galaxyproject.org/Support" ) ) if trans.app.config.track_jobs_in_database: job = trans.sa_session.query( trans.app.model.Job ).get( job_id ) job.stderr = error_msg diff --git a/lib/galaxy/web/base/interactive_environments.py b/lib/galaxy/web/base/interactive_environments.py index 63a8954ac4f..041f6b0ee8b 100644 --- a/lib/galaxy/web/base/interactive_environments.py +++ b/lib/galaxy/web/base/interactive_environments.py @@ -86,7 +86,8 @@ class InteractiveEnviornmentRequest(object): conf_file['galaxy_url'] = self.attr.galaxy_config.galaxy_infrastructure_url.rstrip('/') + '/' else: conf_file['galaxy_url'] = request.application_url.rstrip('/') + '/' - conf_file['galaxy_paster_port'] = self.attr.galaxy_config.guess_galaxy_port() + web_port = self.attr.galaxy_config.galaxy_infrastructure_web_port + conf_file['galaxy_paster_port'] = web_port or self.attr.galaxy_config.guess_galaxy_port() if self.attr.PASSWORD_AUTH: # Generate a random password + salt diff --git a/lib/galaxy/web/framework/webapp.py b/lib/galaxy/web/framework/webapp.py index 2a852eeb7c4..49f350590bc 100644 --- a/lib/galaxy/web/framework/webapp.py +++ b/lib/galaxy/web/framework/webapp.py @@ -668,7 +668,8 @@ class GalaxyWebTransaction( base.DefaultWebTransaction, """ history = None if self.galaxy_session: - history = self.galaxy_session.current_history + if hasattr( self.galaxy_session, 'current_history' ): + history = self.galaxy_session.current_history if not history and util.string_as_bool( create ): history = self.new_history() return history diff --git a/lib/galaxy/webapps/galaxy/api/annotations.py b/lib/galaxy/webapps/galaxy/api/annotations.py index b621bf55b9d..d0dbd931d80 100644 --- a/lib/galaxy/webapps/galaxy/api/annotations.py +++ b/lib/galaxy/webapps/galaxy/api/annotations.py @@ -63,7 +63,7 @@ class HistoryAnnotationsController(BaseAnnotationsController): def _get_item_from_id(self, trans, idstr): decoded_idstr = self.decode_id( idstr ) - history = self.history_manager.get_accessible( trans, decoded_idstr, trans.user ) + history = self.history_manager.get_accessible( decoded_idstr, trans.user, current_history=trans.history ) return history @@ -77,8 +77,8 @@ class HistoryContentAnnotationsController( BaseAnnotationsController ): def _get_item_from_id(self, trans, idstr): decoded_idstr = self.decode_id( idstr ) - hda = self.hda_manager.get_accessible( trans, decoded_idstr, trans.user ) - hda = self.hda_manager.error_if_uploading( trans, hda ) + hda = self.hda_manager.get_accessible( decoded_idstr, trans.user ) + hda = self.hda_manager.error_if_uploading( hda ) return hda diff --git a/lib/galaxy/webapps/galaxy/api/configuration.py b/lib/galaxy/webapps/galaxy/api/configuration.py index d9ff5fcb1cb..2f2ccd74037 100644 --- a/lib/galaxy/webapps/galaxy/api/configuration.py +++ b/lib/galaxy/webapps/galaxy/api/configuration.py @@ -9,6 +9,7 @@ from galaxy.web import require_admin from galaxy.web.base.controller import BaseAPIController from galaxy.managers import base +from galaxy.managers import configuration import logging log = logging.getLogger( __name__ ) @@ -18,8 +19,8 @@ class ConfigurationController( BaseAPIController ): def __init__( self, app ): super( ConfigurationController, self ).__init__( app ) - self.config_serializer = ConfigSerializer( app ) - self.admin_config_serializer = AdminConfigSerializer( app ) + self.config_serializer = configuration.ConfigSerializer( app ) + self.admin_config_serializer = configuration.AdminConfigSerializer( app ) @expose_api_anonymous_and_sessionless def index( self, trans, **kwd ): @@ -29,7 +30,7 @@ class ConfigurationController( BaseAPIController ): Note: a more complete list is returned if the user is an admin. """ - is_admin = self.user_manager.is_admin( trans, trans.user ) + is_admin = self.user_manager.is_admin( trans.user ) serialization_params = self._parse_serialization_params( kwd, 'all' ) return self.get_config_dict( trans, is_admin, **serialization_params ) @@ -57,8 +58,7 @@ class ConfigurationController( BaseAPIController ): #TODO: this should probably just be under a different route: 'admin/configuration' serializer = self.admin_config_serializer - serialized = serializer.serialize_to_view( trans, self.app.config, - view=view, keys=keys, default_view=default_view ) + serialized = serializer.serialize_to_view( self.app.config, view=view, keys=keys, default_view=default_view ) return serialized @expose_api @@ -85,77 +85,6 @@ class ConfigurationController( BaseAPIController ): return rval -#TODO: for lack of a manager file for the config. May well be better in config.py? Circ imports? -class ConfigSerializer( base.ModelSerializer ): - - def __init__( self, app ): - super( ConfigSerializer, self ).__init__( app ) - - self.default_view = 'all' - self.add_view( 'all', self.serializers.keys() ) - - def default_serializer( self, trans, config, key ): - return getattr( config, key, None ) - - def add_serializers( self ): - def _defaults_to( default ): - return lambda t, i, k: getattr( i, k, default ) - - self.serializers = { - #TODO: this is available from user data, remove - 'is_admin_user' : lambda *a: False, - - 'brand' : _defaults_to( '' ), - #TODO: this doesn't seem right - 'logo_url' : lambda t, i, k: self.url_for( i.get( k, '/' ) ), - 'terms_url' : _defaults_to( '' ), - - #TODO: don't hardcode here - hardcode defaults once in config.py - 'wiki_url' : _defaults_to( "http://galaxyproject.org/" ), - 'search_url' : _defaults_to( "http://galaxyproject.org/search/usegalaxy/" ), - 'mailing_lists' : _defaults_to( "http://wiki.galaxyproject.org/MailingLists" ), - 'screencasts_url' : _defaults_to( "http://vimeo.com/galaxyproject" ), - 'citation_url' : _defaults_to( "http://wiki.galaxyproject.org/CitingGalaxy" ), - 'support_url' : _defaults_to( "http://wiki.galaxyproject.org/Support" ), - 'lims_doc_url' : _defaults_to( "http://main.g2.bx.psu.edu/u/rkchak/p/sts" ), - 'biostar_url' : _defaults_to( '' ), - 'biostar_url_redirect' : lambda *a: self.url_for( controller='biostar', action='biostar_redirect', - qualified=True ), - - 'use_remote_user' : _defaults_to( None ), - 'remote_user_logout_href' : _defaults_to( '' ), - 'enable_cloud_launch' : _defaults_to( False ), - 'datatypes_disable_auto' : _defaults_to( False ), - 'allow_user_dataset_purge' : _defaults_to( False ), - 'enable_unique_workflow_defaults' : _defaults_to( False ), - - 'nginx_upload_path' : _defaults_to( self.url_for( controller='api', action='tools' ) ), - 'ftp_upload_dir' : _defaults_to( None ), - 'ftp_upload_site' : _defaults_to( None ), - 'version_major' : _defaults_to( None ), - } - - -class AdminConfigSerializer( ConfigSerializer ): - # config attributes viewable by admin users - - def add_serializers( self ): - super( AdminConfigSerializer, self ).add_serializers() - def _defaults_to( default ): - return lambda t, i, k: getattr( i, k, default ) - - self.serializers.update({ - #TODO: this is available from user data, remove - 'is_admin_user' : lambda *a: True, - - 'library_import_dir' : _defaults_to( None ), - 'user_library_import_dir' : _defaults_to( None ), - 'allow_library_path_paste' : _defaults_to( False ), - 'allow_user_creation' : _defaults_to( False ), - 'allow_user_deletion' : _defaults_to( False ), - }) - - def _tool_conf_to_dict(conf): return dict( config_filename=conf['config_filename'], diff --git a/lib/galaxy/webapps/galaxy/api/dataset_collections.py b/lib/galaxy/webapps/galaxy/api/dataset_collections.py index 2f489618752..9d25976dffd 100644 --- a/lib/galaxy/webapps/galaxy/api/dataset_collections.py +++ b/lib/galaxy/webapps/galaxy/api/dataset_collections.py @@ -45,7 +45,7 @@ class DatasetCollectionsController( if instance_type == "history": history_id = payload.get( 'history_id' ) history_id = self.decode_id( history_id ) - history = self.history_manager.get_owned( trans, history_id, trans.user ) + history = self.history_manager.get_owned( history_id, trans.user, current_history=trans.history ) create_params[ "parent" ] = history elif instance_type == "library": folder_id = payload.get( 'folder_id' ) diff --git a/lib/galaxy/webapps/galaxy/api/datasets.py b/lib/galaxy/webapps/galaxy/api/datasets.py index 0b870c6e366..0f24ba1e2ce 100644 --- a/lib/galaxy/webapps/galaxy/api/datasets.py +++ b/lib/galaxy/webapps/galaxy/api/datasets.py @@ -66,7 +66,8 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): else: # Default: return dataset as dict. if hda_ldda == 'hda': - return self.hda_serializer.serialize_to_view( trans, dataset, view=kwd.get( 'view', 'detailed' ) ) + return self.hda_serializer.serialize_to_view( dataset, + view=kwd.get( 'view', 'detailed' ), user=trans.user, trans=trans ) else: rval = dataset.to_dict() @@ -80,7 +81,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): """ Returns state of dataset. """ - msg = self.hda_manager.data_conversion_status( trans, dataset ) + msg = self.hda_manager.data_conversion_status( dataset ) if not msg: msg = dataset.conversion_messages.DATA @@ -91,7 +92,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): Init-like method that returns state of dataset's converted datasets. Returns valid chroms for that dataset as well. """ - msg = self.hda_manager.data_conversion_status( trans, dataset ) + msg = self.hda_manager.data_conversion_status( dataset ) if msg: return msg @@ -140,7 +141,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): return dataset.conversion_messages.NO_DATA # Dataset check. - msg = self.hda_manager.data_conversion_status( trans, dataset ) + msg = self.hda_manager.data_conversion_status( dataset ) if msg: return msg @@ -233,7 +234,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): be slow because indexes need to be created. """ # Dataset check. - msg = self.hda_manager.data_conversion_status( trans, dataset ) + msg = self.hda_manager.data_conversion_status( dataset ) if msg: return msg @@ -280,7 +281,7 @@ class DatasetsController( BaseAPIController, UsesVisualizationMixin ): rval = '' try: - hda = self.hda_manager.get_accessible( trans, decoded_content_id, trans.user ) + hda = self.hda_manager.get_accessible( decoded_content_id, trans.user ) if raw: if filename and filename != 'index': diff --git a/lib/galaxy/webapps/galaxy/api/extended_metadata.py b/lib/galaxy/webapps/galaxy/api/extended_metadata.py index 4d2db859af0..6d168d184e0 100644 --- a/lib/galaxy/webapps/galaxy/api/extended_metadata.py +++ b/lib/galaxy/webapps/galaxy/api/extended_metadata.py @@ -71,7 +71,7 @@ class HistoryDatasetExtendMetadataController(BaseExtendedMetadataController): def _get_item_from_id(self, trans, idstr, check_writable=True): decoded_idstr = self.decode_id( idstr ) if check_writable: - return self.hda_manager.get_owned( trans, decoded_idstr, trans.user ) + return self.hda_manager.get_owned( decoded_idstr, trans.user, current_history=trans.history ) else: - hda = self.hda_manager.get_accessible( trans, decoded_idstr, trans.user ) - return self.hda_manager.error_if_uploading( trans, hda ) + hda = self.hda_manager.get_accessible( decoded_idstr, trans.user ) + return self.hda_manager.error_if_uploading( hda ) diff --git a/lib/galaxy/webapps/galaxy/api/folder_contents.py b/lib/galaxy/webapps/galaxy/api/folder_contents.py index 267880e9866..19678837d11 100644 --- a/lib/galaxy/webapps/galaxy/api/folder_contents.py +++ b/lib/galaxy/webapps/galaxy/api/folder_contents.py @@ -265,8 +265,8 @@ class FolderContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrary rval = {} try: decoded_hda_id = self.decode_id( from_hda_id ) - hda = self.hda_manager.get_owned( trans, decoded_hda_id, trans.user ) - hda = self.hda_manager.error_if_uploading( trans, hda ) + hda = self.hda_manager.get_owned( decoded_hda_id, trans.user, current_history=trans.history ) + hda = self.hda_manager.error_if_uploading( hda ) folder = self.get_library_folder( trans, encoded_folder_id_16, check_accessible=True ) library = folder.parent_library diff --git a/lib/galaxy/webapps/galaxy/api/histories.py b/lib/galaxy/webapps/galaxy/api/histories.py index 813ef96da83..f1753be7982 100644 --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -103,8 +103,11 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor current_user = self.user_manager.current_user( trans ) if self.user_manager.is_anonymous( current_user ): current_history = self.history_manager.get_current( trans ) + if not current_history: + return [] #note: ignores filters, limit, offset - return [ self.history_serializer.serialize_to_view( trans, current_history, **serialization_params ) ] + return [ self.history_serializer.serialize_to_view( current_history, + user=current_user, trans=trans, **serialization_params ) ] filters = [] # support the old default of not-returning/filtering-out deleted histories @@ -116,11 +119,12 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor #TODO: eventually make order_by a param as well order_by = sqlalchemy.desc( self.app.model.History.create_time ) - histories = self.history_manager.list( trans, filters=filters, order_by=order_by, limit=limit, offset=offset ) + histories = self.history_manager.list( filters=filters, order_by=order_by, limit=limit, offset=offset ) rval = [] for history in histories: - history_dict = self.history_serializer.serialize_to_view( trans, history, **serialization_params ) + history_dict = self.history_serializer.serialize_to_view( history, + user=trans.user, trans=trans, **serialization_params ) rval.append( history_dict ) return rval @@ -174,19 +178,19 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor deleted = string_as_bool( deleted ) if history_id == "most_recently_used": - history = self.history_manager.most_recent( trans, trans.user, - filters=( self.app.model.History.deleted == False ) ) + history = self.history_manager.most_recent( trans.user, + filters=( self.app.model.History.deleted == False ), current_history=trans.history ) else: - history = self.history_manager.get_accessible( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_accessible( self.decode_id( history_id ), trans.user, current_history=trans.history ) - return self.history_serializer.serialize_to_view( trans, history, - **self._parse_serialization_params( kwd, 'detailed' ) ) + return self.history_serializer.serialize_to_view( history, + user=trans.user, trans=trans, **self._parse_serialization_params( kwd, 'detailed' ) ) @expose_api_anonymous def citations( self, trans, history_id, **kwd ): """ """ - history = self.history_manager.get_accessible( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_accessible( self.decode_id( history_id ), trans.user, current_history=trans.history ) tool_ids = set([]) for dataset in history.datasets: job = dataset.creating_job @@ -234,19 +238,19 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor # if a history id was passed, copy that history if copy_this_history_id: decoded_id = self.decode_id( copy_this_history_id ) - original_history = self.history_manager.get_accessible( trans, decoded_id, trans.user ) + original_history = self.history_manager.get_accessible( decoded_id, trans.user, current_history=trans.history ) hist_name = hist_name or ( "Copy of '%s'" % original_history.name ) new_history = original_history.copy( name=hist_name, target_user=trans.user ) # otherwise, create a new empty history else: - new_history = self.history_manager.create( trans, user=trans.user, name=hist_name ) + new_history = self.history_manager.create( user=trans.user, name=hist_name ) trans.sa_session.add( new_history ) trans.sa_session.flush() - return self.history_serializer.serialize_to_view( trans, new_history, - **self._parse_serialization_params( kwd, 'detailed' ) ) + return self.history_serializer.serialize_to_view( new_history, + user=trans.user, trans=trans, **self._parse_serialization_params( kwd, 'detailed' ) ) @expose_api def delete( self, trans, id, **kwd ): @@ -279,13 +283,13 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor if kwd.get( 'payload', None ): purge = string_as_bool( kwd['payload'].get( 'purge', False ) ) - history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) - self.history_manager.delete( trans, history ) + history = self.history_manager.get_owned( self.decode_id( history_id ), trans.user, current_history=trans.history ) + self.history_manager.delete( history ) if purge: - self.history_manager.purge( trans, history ) + self.history_manager.purge( history ) - return self.history_serializer.serialize_to_view( trans, history, - **self._parse_serialization_params( kwd, 'detailed' ) ) + return self.history_serializer.serialize_to_view( history, + user=trans.user, trans=trans, **self._parse_serialization_params( kwd, 'detailed' ) ) @expose_api def undelete( self, trans, id, **kwd ): @@ -304,11 +308,11 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor :returns: 'OK' if the history was undeleted """ history_id = id - history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) - self.history_manager.undelete( trans, history ) + history = self.history_manager.get_owned( self.decode_id( history_id ), trans.user, current_history=trans.history ) + self.history_manager.undelete( history ) - return self.history_serializer.serialize_to_view( trans, history, - **self._parse_serialization_params( kwd, 'detailed' ) ) + return self.history_serializer.serialize_to_view( history, + user=trans.user, trans=trans, **self._parse_serialization_params( kwd, 'detailed' ) ) @expose_api def update( self, trans, id, payload, **kwd ): @@ -333,11 +337,11 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor any values that were different from the original and, therefore, updated """ #TODO: PUT /api/histories/{encoded_history_id} payload = { rating: rating } (w/ no security checks) - history = self.history_manager.get_owned( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_owned( self.decode_id( id ), trans.user, current_history=trans.history ) - self.history_deserializer.deserialize( trans, history, payload ) - return self.history_serializer.serialize_to_view( trans, history, - **self._parse_serialization_params( kwd, 'detailed' ) ) + self.history_deserializer.deserialize( history, payload, user=trans.user, trans=trans ) + return self.history_serializer.serialize_to_view( history, + user=trans.user, trans=trans, **self._parse_serialization_params( kwd, 'detailed' ) ) @expose_api def archive_export( self, trans, id, **kwds ): @@ -355,7 +359,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor """ # PUT instead of POST because multiple requests should just result # in one object being created. - history = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_accessible( self.decode_id( id ), trans.user, current_history=trans.history ) jeha = history.latest_export up_to_date = jeha and jeha.up_to_date if 'force' in kwds: @@ -388,7 +392,7 @@ class HistoriesController( BaseAPIController, ExportsHistoryMixin, ImportsHistor """ # Seems silly to put jeha_id in here, but want GET to be immuatable? # and this is being accomplished this way. - history = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_accessible( self.decode_id( id ), trans.user, current_history=trans.history ) matching_exports = filter( lambda e: trans.security.encode_id( e.id ) == jeha_id, history.exports ) if not matching_exports: raise exceptions.ObjectNotFound() diff --git a/lib/galaxy/webapps/galaxy/api/history_contents.py b/lib/galaxy/webapps/galaxy/api/history_contents.py index 6b8118f9501..95817c72131 100644 --- a/lib/galaxy/webapps/galaxy/api/history_contents.py +++ b/lib/galaxy/webapps/galaxy/api/history_contents.py @@ -68,7 +68,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar """ rval = [] - history = self.history_manager.get_accessible( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_accessible( self.decode_id( history_id ), trans.user, current_history=trans.history ) # Allow passing in type or types - for continuity rest of methods # take in type - but this one can be passed multiple types and @@ -102,7 +102,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar if isinstance( content, trans.app.model.HistoryDatasetAssociation ): view = 'detailed' if detailed else 'summary' - hda_dict = self.hda_serializer.serialize_to_view( trans, content, view=view ) + hda_dict = self.hda_serializer.serialize_to_view( content, view=view, user=trans.user, trans=trans ) rval.append( hda_dict ) elif isinstance( content, trans.app.model.HistoryDatasetCollectionAssociation ): @@ -132,7 +132,7 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar :rtype: dict :returns: dictionary containing detailed HDA information """ - history = self.history_manager.get_accessible( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_accessible( self.decode_id( history_id ), trans.user, current_history=trans.history ) contents_type = kwd.get('type', 'dataset') if contents_type == 'dataset': @@ -143,9 +143,9 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar return self.__handle_unknown_contents_type( trans, contents_type ) def __show_dataset( self, trans, id, **kwd ): - hda = self.hda_manager.get_accessible( trans, self.decode_id( id ), trans.user ) - return self.hda_serializer.serialize_to_view( trans, hda, - **self._parse_serialization_params( kwd, 'detailed' ) ) + hda = self.hda_manager.get_accessible( self.decode_id( id ), trans.user ) + return self.hda_serializer.serialize_to_view( hda, + user=trans.user, trans=trans, **self._parse_serialization_params( kwd, 'detailed' ) ) def __show_dataset_collection( self, trans, id, history_id, **kwd ): try: @@ -214,7 +214,8 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # TODO: Flush out create new collection documentation above, need some # examples. See also bioblend and API tests for specific examples. - history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_owned( self.decode_id( history_id ), trans.user, + current_history=trans.history ) type = payload.get( 'type', 'dataset' ) if type == 'dataset': @@ -247,17 +248,17 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # copy an existing, accessible hda elif source == 'hda': unencoded_hda_id = self.decode_id( content ) - original = self.hda_manager.get_accessible( trans, unencoded_hda_id, trans.user ) + original = self.hda_manager.get_accessible( unencoded_hda_id, trans.user ) # check for access on history that contains the original hda as well - self.history_manager.error_unless_accessible( trans, original.history, trans.user ) + self.history_manager.error_unless_accessible( original.history, trans.user, current_history=trans.history ) data_copy = original.copy( copy_children=True ) hda = history.add_dataset( data_copy ) trans.sa_session.flush() if not hda: return None - return self.hda_serializer.serialize_to_view( trans, hda, - **self._parse_serialization_params( kwd, 'detailed' ) ) + return self.hda_serializer.serialize_to_view( hda, + user=trans.user, trans=trans, **self._parse_serialization_params( kwd, 'detailed' ) ) def __create_dataset_collection( self, trans, history, payload, **kwd ): source = kwd.get("source", "new_collection") @@ -319,10 +320,10 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # anon user: ensure that history ids match up and the history is the current, # check for uploading, and use only the subset of attribute keys manipulatable by anon users if trans.user == None: - hda = self.hda_manager.by_id( trans, self.decode_id( id ) ) + hda = self.hda_manager.by_id( self.decode_id( id ) ) if hda.history != trans.history: raise exceptions.AuthenticationRequired( 'API authentication required for this request' ) - hda = self.hda_manager.error_if_uploading( trans, hda ) + hda = self.hda_manager.error_if_uploading( hda ) anon_allowed_payload = {} if 'deleted' in payload: @@ -333,22 +334,22 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # logged in user: use full payload, check state if deleting, and make sure the history is theirs else: - hda = self.hda_manager.get_owned( trans, self.decode_id( id ), trans.user ) + hda = self.hda_manager.get_owned( self.decode_id( id ), trans.user, current_history=trans.history ) # only check_state if not deleting, otherwise cannot delete uploading files check_state = not payload.get( 'deleted', False ) if check_state: - hda = self.hda_manager.error_if_uploading( trans, hda ) + hda = self.hda_manager.error_if_uploading( hda ) # make the actual changes #TODO: is this if still needed? if hda and isinstance( hda, trans.model.HistoryDatasetAssociation ): - self.hda_deserializer.deserialize( trans, hda, payload ) + self.hda_deserializer.deserialize( hda, payload, user=trans.user, trans=trans ) #TODO: this should be an effect of deleting the hda if payload.get( 'deleted', False ): self.hda_manager.stop_creating_job( hda ) - return self.hda_serializer.serialize_to_view( trans, hda, - **self._parse_serialization_params( kwd, 'detailed' ) ) + return self.hda_serializer.serialize_to_view( hda, + user=trans.user, trans=trans, **self._parse_serialization_params( kwd, 'detailed' ) ) return {} @@ -399,15 +400,15 @@ class HistoryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # payload takes priority purge = util.string_as_bool( kwd['payload'].get( 'purge', purge ) ) - hda = self.hda_manager.get_owned( trans, self.decode_id( id ), trans.user ) - self.hda_manager.error_if_uploading( trans, hda ) + hda = self.hda_manager.get_owned( self.decode_id( id ), trans.user, current_history=trans.history ) + self.hda_manager.error_if_uploading( hda ) if purge: - self.hda_manager.purge( trans, hda ) + self.hda_manager.purge( hda ) else: - self.hda_manager.delete( trans, hda ) - return self.hda_serializer.serialize_to_view( trans, hda, - **self._parse_serialization_params( kwd, 'detailed' ) ) + self.hda_manager.delete( hda ) + return self.hda_serializer.serialize_to_view( hda, + user=trans.user, trans=trans, **self._parse_serialization_params( kwd, 'detailed' ) ) def __handle_unknown_contents_type( self, trans, contents_type ): raise exceptions.UnknownContentsType('Unknown contents type: %s' % type) diff --git a/lib/galaxy/webapps/galaxy/api/job_files.py b/lib/galaxy/webapps/galaxy/api/job_files.py index 27445beac38..4b21c1ceb5b 100644 --- a/lib/galaxy/webapps/galaxy/api/job_files.py +++ b/lib/galaxy/webapps/galaxy/api/job_files.py @@ -83,9 +83,21 @@ class JobFilesAPIController( BaseAPIController ): self.__check_job_can_write_to_path( trans, job, path ) # Is this writing an unneeded file? Should this just copy in Python? - input_file = payload.get( "file", payload.get( "__file", None ) ).file + if '__file_path' in payload: + file_path = payload.get( '__file_path' ) + upload_store = trans.app.config.nginx_upload_job_files_store + assert upload_store, ( "Request appears to have been processed by" + " nginx_upload_module but Galaxy is not" + " configured to recognize it" ) + assert file_path.startswith( upload_store ), \ + ( "Filename provided by nginx (%s) is not in correct" + " directory (%s)" % ( file_path, upload_store ) ) + input_file = open( file_path ) + else: + input_file = payload.get( "file", + payload.get( "__file", None ) ).file try: - shutil.copyfile( input_file.name, path ) + shutil.move( input_file.name, path ) finally: input_file.close() return {"message": "ok"} diff --git a/lib/galaxy/webapps/galaxy/api/jobs.py b/lib/galaxy/webapps/galaxy/api/jobs.py index 4bd0cdbc662..193179dc9e2 100644 --- a/lib/galaxy/webapps/galaxy/api/jobs.py +++ b/lib/galaxy/webapps/galaxy/api/jobs.py @@ -262,7 +262,7 @@ class JobController( BaseAPIController, UsesLibraryMixinItems ): if 'id' in v: if 'src' not in v or v[ 'src' ] == 'hda': hda_id = self.decode_id( v['id'] ) - dataset = self.hda_manager.get_accessible( trans, hda_id, trans.user ) + dataset = self.hda_manager.get_accessible( hda_id, trans.user ) else: dataset = self.get_library_dataset_dataset_association( trans, v['id'] ) if dataset is None: diff --git a/lib/galaxy/webapps/galaxy/api/lda_datasets.py b/lib/galaxy/webapps/galaxy/api/lda_datasets.py index 1af9d8d6c14..7bac9209734 100644 --- a/lib/galaxy/webapps/galaxy/api/lda_datasets.py +++ b/lib/galaxy/webapps/galaxy/api/lda_datasets.py @@ -33,7 +33,7 @@ class LibraryDatasetsController( BaseAPIController, UsesVisualizationMixin ): def __init__( self, app ): super( LibraryDatasetsController, self ).__init__( app ) self.folder_manager = folders.FolderManager() - self.role_manager = roles.RoleManager() + self.role_manager = roles.RoleManager( app ) @expose_api_anonymous def show( self, trans, id, **kwd ): diff --git a/lib/galaxy/webapps/galaxy/api/libraries.py b/lib/galaxy/webapps/galaxy/api/libraries.py index bd3dfb90a71..d423c003b16 100644 --- a/lib/galaxy/webapps/galaxy/api/libraries.py +++ b/lib/galaxy/webapps/galaxy/api/libraries.py @@ -18,7 +18,7 @@ class LibrariesController( BaseAPIController ): super( LibrariesController, self ).__init__( app ) self.folder_manager = folders.FolderManager() self.library_manager = libraries.LibraryManager() - self.role_manager = roles.RoleManager() + self.role_manager = roles.RoleManager( app ) @expose_api_anonymous def index( self, trans, **kwd ): diff --git a/lib/galaxy/webapps/galaxy/api/library_contents.py b/lib/galaxy/webapps/galaxy/api/library_contents.py index 55dff652056..d0e05dd8c15 100644 --- a/lib/galaxy/webapps/galaxy/api/library_contents.py +++ b/lib/galaxy/webapps/galaxy/api/library_contents.py @@ -304,8 +304,8 @@ class LibraryContentsController( BaseAPIController, UsesLibraryMixin, UsesLibrar # check permissions on (all three?) resources: hda, library, folder # TODO: do we really need the library?? from_hda_id = self.decode_id( from_hda_id ) - hda = self.hda_manager.get_owned( trans, from_hda_id, trans.user ) - hda = self.hda_manager.error_if_uploading( trans, hda ) + hda = self.hda_manager.get_owned( from_hda_id, trans.user, current_history=trans.history ) + hda = self.hda_manager.error_if_uploading( hda ) # library = self.get_library( trans, library_id, check_accessible=True ) folder = self.get_library_folder( trans, folder_id, check_accessible=True ) diff --git a/lib/galaxy/webapps/galaxy/api/provenance.py b/lib/galaxy/webapps/galaxy/api/provenance.py index 1a6678b39e3..4b9500833d2 100644 --- a/lib/galaxy/webapps/galaxy/api/provenance.py +++ b/lib/galaxy/webapps/galaxy/api/provenance.py @@ -41,7 +41,7 @@ class BaseProvenanceController( BaseAPIController ): def _get_provenance( self, trans, item_class_name, item_id, follow=True ): provenance_item = self.get_object( trans, item_id, item_class_name, check_ownership=False, check_accessible=False) if item_class_name == "HistoryDatasetAssociation": - self.hda_manager.error_unless_accessible( trans, provenance_item, trans.user ) + self.hda_manager.error_unless_accessible( provenance_item, trans.user ) else: self.security_check( trans, provenance_item, check_accessible=True ) out = self._get_record( trans, provenance_item, follow ) diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py index 6400188a76b..27b08ffab44 100644 --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -240,7 +240,7 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): history_id = payload.get("history_id", None) if history_id: decoded_id = self.decode_id( history_id ) - target_history = self.history_manager.get_owned( trans, decoded_id, trans.user ) + target_history = self.history_manager.get_owned( decoded_id, trans.user, current_history=trans.history ) else: target_history = None @@ -386,9 +386,9 @@ class ToolsController( BaseAPIController, UsesVisualizationMixin ): # Dataset check. decoded_dataset_id = self.decode_id( payload.get( 'target_dataset_id' ) ) - original_dataset = self.hda_manager.get_accessible( trans, decoded_dataset_id, user=trans.user ) - original_dataset = self.hda_manager.error_if_uploading( trans, original_dataset ) - msg = self.hda_manager.data_conversion_status( trans, original_dataset ) + original_dataset = self.hda_manager.get_accessible( decoded_dataset_id, user=trans.user ) + original_dataset = self.hda_manager.error_if_uploading( original_dataset ) + msg = self.hda_manager.data_conversion_status( original_dataset ) if msg: return msg diff --git a/lib/galaxy/webapps/galaxy/api/workflows.py b/lib/galaxy/webapps/galaxy/api/workflows.py index 24014ff3683..25a5705fb3c 100644 --- a/lib/galaxy/webapps/galaxy/api/workflows.py +++ b/lib/galaxy/webapps/galaxy/api/workflows.py @@ -152,7 +152,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn if 'from_history_id' in payload: from_history_id = payload.get( 'from_history_id' ) from_history_id = self.decode_id( from_history_id ) - history = self.history_manager.get_accessible( trans, from_history_id, trans.user ) + history = self.history_manager.get_accessible( from_history_id, trans.user, current_history=trans.history ) job_ids = map( self.decode_id, payload.get( 'job_ids', [] ) ) dataset_ids = payload.get( 'dataset_ids', [] ) @@ -318,10 +318,10 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn tool_version = payload.get( 'tool_version', None ) tool_inputs = payload.get( 'inputs', None ) annotation = payload.get( 'annotation', '' ) - + # load tool tool = self._get_tool( tool_id, tool_version=tool_version, user=trans.user ) - + # initialize module trans.workflow_building_mode = True module = module_factory.from_dict( trans, { @@ -329,7 +329,7 @@ class WorkflowsAPIController(BaseAPIController, UsesStoredWorkflowMixin, UsesAnn 'tool_id' : tool.id, 'tool_state' : None } ) - + # create tool model and default tool state (if missing) tool_model = module.tool.to_json(trans, tool_inputs, is_workflow=True) module.state.inputs = copy.deepcopy(tool_model['state_inputs']) diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index 27986d71baa..f6148079644 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -103,13 +103,15 @@ def app_factory( global_conf, **kwargs ): webapp = wrap_in_static( webapp, global_conf, plugin_frameworks=[ app.visualizations_registry ], **kwargs ) # Close any pooled database connections before forking try: - galaxy.model.mapping.metadata.engine.connection_provider._pool.dispose() + galaxy.model.mapping.metadata.bind.dispose() except: - pass + log.exception("Unable to dispose of pooled galaxy model database connections.") try: - galaxy.model.tool_shed_install.mapping.metadata.engine.connection_provider._pool.dispose() + # This model may not actually be bound. + if galaxy.model.tool_shed_install.mapping.metadata.bind: + galaxy.model.tool_shed_install.mapping.metadata.bind.dispose() except: - pass + log.exception("Unable to dispose of pooled toolshed install model database connections.") if not app.config.is_uwsgi: postfork_setup() diff --git a/lib/galaxy/webapps/galaxy/controllers/dataset.py b/lib/galaxy/webapps/galaxy/controllers/dataset.py index 992e2ac5900..c81c88db085 100644 --- a/lib/galaxy/webapps/galaxy/controllers/dataset.py +++ b/lib/galaxy/webapps/galaxy/controllers/dataset.py @@ -529,8 +529,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def get_name_and_link_async( self, trans, id=None ): """ Returns dataset's name and link. """ decoded_id = self.decode_id( id ) - dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( trans, dataset ) + dataset = self.hda_manager.get_accessible( decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) return_dict = { "name" : dataset.name, "link" : url_for( controller='dataset', action="display_by_username_and_slug", username=dataset.history.user.username, slug=trans.security.encode_id( dataset.id ) ) } return return_dict @@ -538,8 +538,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def get_embed_html_async( self, trans, id ): """ Returns HTML for embedding a dataset in a page. """ decoded_id = self.decode_id( id ) - dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( trans, dataset ) + dataset = self.hda_manager.get_accessible( decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if dataset: return "Embedded Dataset '%s'" % dataset.name @@ -556,8 +556,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses """ Rate a dataset asynchronously and return updated community data. """ decoded_id = self.decode_id( id ) - dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( trans, dataset ) + dataset = self.hda_manager.get_accessible( decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if not dataset: return trans.show_error_message( "The specified dataset does not exist." ) @@ -571,8 +571,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses """ Display dataset by username and slug; because datasets do not yet have slugs, the slug is the dataset's id. """ id = slug decoded_id = self.decode_id( id ) - dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( trans, dataset ) + dataset = self.hda_manager.get_accessible( decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if dataset: # Filename used for composite types. if filename: @@ -614,8 +614,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses """ Returns item content in HTML format. """ decoded_id = self.decode_id( id ) - dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( trans, dataset ) + dataset = self.hda_manager.get_accessible( decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if dataset is None: raise web.httpexceptions.HTTPNotFound() truncated, dataset_data = self.hda_manager.text_data( dataset, preview=True ) @@ -627,8 +627,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses def annotate_async( self, trans, id, new_annotation=None, **kwargs ): #TODO:?? why is this an access check only? decoded_id = self.decode_id( id ) - dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( trans, dataset ) + dataset = self.hda_manager.get_accessible( decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if not dataset: web.httpexceptions.HTTPNotFound() if dataset and new_annotation: @@ -641,8 +641,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses @web.expose def get_annotation_async( self, trans, id ): decoded_id = self.decode_id( id ) - dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( trans, dataset ) + dataset = self.hda_manager.get_accessible( decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) if not dataset: web.httpexceptions.HTTPNotFound() annotation = self.get_item_annotation_str( trans.sa_session, trans.user, dataset ) @@ -859,22 +859,24 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses status = 'done' try: id = trans.app.security.decode_id( dataset_id ) - history = trans.get_history() user = trans.get_user() hda = trans.sa_session.query( self.app.model.HistoryDatasetAssociation ).get( id ) # Invalid HDA assert hda, 'Invalid history dataset ID' + # Walk up parent datasets to find the containing history + history = trans.get_history() topmost_parent = hda while topmost_parent.parent: topmost_parent = topmost_parent.parent - assert topmost_parent in history.datasets, "Data does not belong to current history" # If the user is anonymous, make sure the HDA is owned by the current session. if not user: - assert trans.galaxy_session.current_history_id == trans.history.id, 'Invalid history dataset ID' + current_history_id = trans.galaxy_session.current_history_id + assert topmost_parent.history.id == current_history_id, 'Data does not belong to current user' # If the user is known, make sure the HDA is owned by the current user. else: - assert topmost_parent.history.user == trans.user, 'Invalid history dataset ID' + assert topmost_parent.history.user == user, 'Data does not belong to current user' + # Ensure HDA is deleted hda.deleted = True # HDA is purgeable @@ -897,8 +899,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses except: log.exception( 'Unable to purge dataset (%s) on purge of HDA (%s):' % ( hda.dataset.id, hda.id ) ) trans.sa_session.flush() - except Exception: - msg = 'HDA purge failed (encoded: %s, decoded: %s)' % ( dataset_id, id ) + except Exception, exc: + msg = 'HDA purge failed (encoded: %s, decoded: %s): %s' % ( dataset_id, id, exc ) log.exception( msg ) trans.log_event( msg ) message = 'Dataset removal from disk failed' @@ -1023,7 +1025,7 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses user = trans.get_user() if source_history is not None: decoded_source_history_id = self.decode_id( source_history ) - history = self.history_manager.get_owned( trans, decoded_source_history_id, trans.user ) + history = self.history_manager.get_owned( decoded_source_history_id, trans.user, current_history=trans.history ) current_history = trans.get_history() else: history = current_history = trans.get_history() @@ -1129,8 +1131,8 @@ class DatasetInterface( BaseUIController, UsesAnnotations, UsesItemRatings, Uses error_msg = error_msg + "You do not have permission to add datasets to %i requested histories. " % ( len( target_histories ) ) for dataset_id in dataset_ids: decoded_id = self.decode_id( dataset_id ) - data = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) - data = self.hda_manager.error_if_uploading( trans, data ) + data = self.hda_manager.get_accessible( decoded_id, trans.user ) + data = self.hda_manager.error_if_uploading( data ) if data is None: error_msg = error_msg + "You tried to copy a dataset that does not exist or that you do not have access to. " diff --git a/lib/galaxy/webapps/galaxy/controllers/history.py b/lib/galaxy/webapps/galaxy/controllers/history.py index 727b0a2e7f3..617bb8ede97 100644 --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -254,7 +254,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI return self.rename( trans, **kwargs ) if operation == "view": decoded_id = self.decode_id( kwargs.get( 'id', None ) ) - history = self.history_manager.get_owned( trans, decoded_id, trans.user ) + history = self.history_manager.get_owned( decoded_id, trans.user, current_history=trans.history ) return trans.response.send_redirect( url_for( controller='history', action='view', id=kwargs['id'], @@ -268,7 +268,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI # Load the histories and ensure they all belong to the current user histories = [] for history_id in history_ids: - history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_owned( self.decode_id( history_id ), trans.user, current_history=trans.history ) if history: # Ensure history is owned by current user if history.user_id != None and trans.user: @@ -313,7 +313,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI if history_ids: histories = [] for history_id in history_ids: - history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_owned( self.decode_id( history_id ), trans.user, current_history=trans.history ) if history.importable: history.importable = False histories.append( history ) @@ -439,7 +439,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI operation = kwargs['operation'].lower() if operation == "view": # Display history. - history = self.history_manager.get_accessible( trans, self.decode_id( ids[0] ), trans.user ) + history = self.history_manager.get_accessible( self.decode_id( ids[0] ), trans.user, current_history=trans.history ) return self.display_by_username_and_slug( trans, history.user.username, history.slug ) elif operation == "copy": if not ids: @@ -455,7 +455,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI for id in ids: # No need to check security, association below won't yield a # hit if this user isn't having the history shared with her. - history = self.history_manager.by_id( trans, self.decode_id( id ) ) + history = self.history_manager.by_id( self.decode_id( id ) ) # Current user is the user with which the histories were shared association = ( trans.sa_session.query( trans.app.model.HistoryUserShareAssociation ) .filter_by( user=trans.user, history=history ).one() ) @@ -555,7 +555,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI unencoded_history_id = trans.history.id if id: unencoded_history_id = self.decode_id( id ) - history_to_view = self.history_manager.get_accessible( trans, unencoded_history_id, trans.user ) + history_to_view = self.history_manager.get_accessible( unencoded_history_id, trans.user, current_history=trans.history ) history_data = self.history_manager._get_history_data( trans, history_to_view ) history_dictionary = history_data[ 'history' ] @@ -595,7 +595,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI hda_dictionaries = [] user_is_owner = False try: - history_to_view = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) + history_to_view = self.history_manager.get_accessible( self.decode_id( id ), trans.user, current_history=trans.history ) user_is_owner = history_to_view.user == trans.user # include all datasets: hidden, deleted, and purged @@ -641,8 +641,9 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI current_history_id = trans.security.encode_id( current_history.id ) if current_history else None history_dictionaries = [] - for history in self.history_manager.by_user( trans, trans.user, filters=deleted_filter ): - history_dictionary = self.history_serializer.serialize_to_view( trans, history, view='detailed' ) + for history in self.history_manager.by_user( trans.user, filters=deleted_filter ): + history_dictionary = self.history_serializer.serialize_to_view( history, + view='detailed', user=trans.user, trans=trans ) history_dictionaries.append( history_dictionary ) return trans.fill_template_mako( "history/view_multiple.mako", @@ -661,7 +662,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI if history is None: raise web.httpexceptions.HTTPNotFound() # Security check raises error if user cannot access history. - self.history_manager.error_unless_accessible( trans, history, trans.user ) + self.history_manager.error_unless_accessible( history, trans.user, current_history=trans.history ) # Get rating data. user_item_rating = 0 @@ -701,7 +702,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI if id: ids = galaxy.util.listify( id ) if ids: - histories = [ self.history_manager.get_accessible( trans, self.decode_id( history_id ), trans.user ) + histories = [ self.history_manager.get_accessible( self.decode_id( history_id ), trans.user, current_history=trans.history ) for history_id in ids ] elif not histories: histories = [ trans.history ] @@ -771,7 +772,9 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI send_to_err = err_msg histories = [] for history_id in id: - histories.append( self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) ) + history_id = self.decode_id( history_id ) + history = self.history_manager.get_owned( history_id, trans.user, current_history=trans.history ) + histories.append( history ) return trans.fill_template( "/history/share.mako", histories=histories, email=email, @@ -894,7 +897,9 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI ids = galaxy.util.listify( ids ) histories = [] for history_id in ids: - histories.append( self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) ) + history_id = self.decode_id( history_id ) + history = self.history_manager.get_owned( history_id, trans.user, current_history=trans.history ) + histories.append( ) return histories def _get_users( self, trans, user, emails_or_ids ): @@ -908,14 +913,14 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI send_to_user = None if '@' in string: email_address = string - send_to_user = self.user_manager.by_email( trans, email_address, + send_to_user = self.user_manager.by_email( email_address, filters=[ trans.app.model.User.table.c.deleted == False ] ) else: user_id = string try: decoded_user_id = self.decode_id( string ) - send_to_user = self.user_manager.by_id( trans, decoded_user_id ) + send_to_user = self.user_manager.by_id( decoded_user_id ) if send_to_user.deleted: send_to_user = None #TODO: in an ideal world, we would let this bubble up to web.expose which would handle it @@ -1144,7 +1149,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI trans.app.job_manager.job_stop_queue.put( job.id ) # Regardless of whether it was previously deleted, get the most recent history or create a new one. - most_recent_history = self.history_manager.most_recent( trans, user=trans.user ) + most_recent_history = self.history_manager.most_recent( user=trans.user ) if most_recent_history: trans.set_history( most_recent_history ) return trans.show_ok_message( "History deleted, your most recent history is now active", @@ -1188,7 +1193,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI @web.json def rate_async( self, trans, id, rating ): """ Rate a history asynchronously and return updated community data. """ - history = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_accessible( self.decode_id( id ), trans.user, current_history=trans.history ) if not history: return trans.show_error_message( "The specified history does not exist." ) # Rate history. @@ -1232,7 +1237,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI # Get history to export. # if id: - history = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_accessible( self.decode_id( id ), trans.user, current_history=trans.history ) else: # Use current history. history = trans.history @@ -1265,7 +1270,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI @web.require_login( "get history name and link" ) def get_name_and_link_async( self, trans, id=None ): """ Returns history's name and link. """ - history = self.history_manager.get_accessible( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_accessible( self.decode_id( id ), trans.user, current_history=trans.history ) if self.create_item_slug( trans.sa_session, history ): trans.sa_session.flush() return_dict = { @@ -1279,7 +1284,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI @web.require_login( "set history's accessible flag" ) def set_accessible_async( self, trans, id=None, accessible=False ): """ Set history's importable attribute and slug. """ - history = self.history_manager.get_owned( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_owned( self.decode_id( id ), trans.user, current_history=trans.history ) # Only set if importable value would change; this prevents a change in the update_time unless attribute really changed. importable = accessible in ['True', 'true', 't', 'T']; if history and history.importable != importable: @@ -1327,11 +1332,11 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI # Do import. if not id: return trans.show_error_message( "You must specify a history you want to import.
You can %s." % referer_message, use_panels=True ) - import_history = self.history_manager.by_id( trans, self.decode_id( id ) ) + import_history = self.history_manager.by_id( self.decode_id( id ) ) if not import_history: return trans.show_error_message( "The specified history does not exist.
You can %s." % referer_message, use_panels=True ) # History is importable if user is admin or it's accessible. TODO: probably want to have app setting to enable admin access to histories. - if not self.history_manager.is_accessible( trans, import_history, trans.user ): + if not self.history_manager.is_accessible( import_history, trans.user, current_history=trans.history ): return trans.show_error_message( "You cannot access this history.
You can %s." % referer_message, use_panels=True ) if user: #dan: I can import my own history. @@ -1397,7 +1402,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI histories = [] for history_id in id: - history = self.history_manager.get_owned( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_owned( self.decode_id( history_id ), trans.user, current_history=trans.history ) if history and history.user_id == user.id: histories.append( history ) if not name or len( histories ) != len( name ): @@ -1454,7 +1459,7 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI ids = galaxy.util.listify( id ) histories = [] for history_id in ids: - history = self.history_manager.get_accessible( trans, self.decode_id( history_id ), trans.user ) + history = self.history_manager.get_accessible( self.decode_id( history_id ), trans.user, current_history=trans.history ) histories.append( history ) user = trans.get_user() for history in histories: @@ -1491,13 +1496,13 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI return trans.response.send_redirect( url_for( "/" ) ) def get_item( self, trans, id ): - return self.history_manager.get_owned( trans, self.decode_id( id ), trans.user ) + return self.history_manager.get_owned( self.decode_id( id ), trans.user, current_history=trans.history ) #TODO: override of base ui controller? def history_data( self, trans, history ): """ """ - return self.history_serializer.serialize_to_view( trans, history, view='detailed' ) + return self.history_serializer.serialize_to_view( history, view='detailed', user=trans.user, trans=trans ) #TODO: combine these next two - poss. with a redirect flag #@web.require_login( "switch to a history" ) @@ -1506,9 +1511,9 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI """ """ try: - history = self.history_manager.get_owned( trans, self.decode_id( id ), trans.user ) + history = self.history_manager.get_owned( self.decode_id( id ), trans.user, current_history=trans.history ) trans.set_history( history ) - return self.history_serializer.serialize_to_view( trans, history, view='detailed' ) + return self.history_serializer.serialize_to_view( history, view='detailed', user=trans.user, trans=trans ) except exceptions.MessageException, msg_exc: trans.response.status = msg_exc.err_code.code return { 'err_msg': msg_exc.err_msg, 'err_code': msg_exc.err_code.code } @@ -1518,13 +1523,13 @@ class HistoryController( BaseUIController, SharableMixin, UsesAnnotations, UsesI """ """ history = trans.get_history( create=True ) - return self.history_serializer.serialize_to_view( trans, history, view='detailed' ) + return self.history_serializer.serialize_to_view( history, view='detailed', user=trans.user, trans=trans ) @web.json def create_new_current( self, trans, name=None ): """ """ new_history = trans.new_history( name ) - return self.history_serializer.serialize_to_view( trans, new_history, view='detailed' ) + return self.history_serializer.serialize_to_view( new_history, view='detailed', user=trans.user, trans=trans ) #TODO: /history/current to do all of the above: if ajax, return json; if post, read id and set to current diff --git a/lib/galaxy/webapps/galaxy/controllers/library_common.py b/lib/galaxy/webapps/galaxy/controllers/library_common.py index 1a7bfd80f6a..2206c586baa 100644 --- a/lib/galaxy/webapps/galaxy/controllers/library_common.py +++ b/lib/galaxy/webapps/galaxy/controllers/library_common.py @@ -1432,7 +1432,6 @@ class LibraryCommon( BaseUIController, UsesFormDefinitionsMixin, UsesExtendedMet message = 'Select at least one dataset from the list of active datasets in your current history' status = 'error' upload_option = kwd.get( 'upload_option', 'import_from_history' ) - widgets = self._get_populated_widgets( folder ) # Send list of data formats to the upload form so the "extension" select list can be populated dynamically file_formats = trans.app.datatypes_registry.upload_file_formats # Send list of genome builds to the form so the "dbkey" select list can be populated dynamically @@ -1459,7 +1458,7 @@ class LibraryCommon( BaseUIController, UsesFormDefinitionsMixin, UsesExtendedMet last_used_build=last_used_build, roles_select_list=roles_select_list, history=history, - widgets=widgets, + widgets=[], template_id=template_id, space_to_tab=space_to_tab, link_data_only=link_data_only, @@ -1515,16 +1514,6 @@ class LibraryCommon( BaseUIController, UsesFormDefinitionsMixin, UsesExtendedMet upload_option_select_list.add_option( option_label, option_value, selected=option_value==upload_option ) return upload_option_select_list - def _get_populated_widgets( self, folder ): - # See if we have any inherited templates. - info_association, inherited = folder.get_info_association( inherited=True ) - if info_association and info_association.inheritable: - widgets = folder.get_template_widgets( trans, get_contents=True ) - # Retain contents of widget fields when form was submitted via refresh_on_change. - return self.populate_widgets_from_kwd( trans, widgets, **kwd ) - else: - return [] - @web.expose def download_dataset_from_folder( self, trans, cntrller, id, library_id=None, **kwd ): """Catches the dataset id and displays file contents as directed""" @@ -2584,8 +2573,8 @@ class LibraryCommon( BaseUIController, UsesFormDefinitionsMixin, UsesExtendedMet # Deny access if the user is not an admin and does not have the LIBRARY_MANAGE and DATASET_MANAGE_PERMISSIONS permissions. if not ( is_admin or \ ( trans.app.security_agent.can_manage_library_item( current_user_roles, item ) and - trans.app.security_agent.can_manage_dataset( current_user_roles, library_dataset.library_dataset_dataset_association.dataset ) ) ): - message = "You are not authorized to manage permissions on library dataset (%s)." % escape( library_dataset.name ) + trans.app.security_agent.can_manage_dataset( current_user_roles, item.library_dataset_dataset_association.dataset ) ) ): + message = "You are not authorized to manage permissions on library dataset (%s)." % escape( item.name ) if cntrller == 'api': return 403, message return trans.response.send_redirect( web.url_for( controller='library_common', diff --git a/lib/galaxy/webapps/galaxy/controllers/mobile.py b/lib/galaxy/webapps/galaxy/controllers/mobile.py index 592d424046a..b0fa11cbd19 100644 --- a/lib/galaxy/webapps/galaxy/controllers/mobile.py +++ b/lib/galaxy/webapps/galaxy/controllers/mobile.py @@ -53,17 +53,21 @@ class Mobile( BaseUIController ): # trans.log_event( "User logged out" ) # trans.handle_user_logout() - def __login( self, trans, email="", password="" ): + def __login(self, trans, login="", password=""): return trans.response.send_redirect( web.url_for(controller='root', action='index' ) ) # error = password_error = None - # user = trans.sa_session.query( model.User ).filter_by( email = email ).first() + # user = trans.sa_session.query( model.User ).filter(or_( + # email == login, + # username == login + # )).first() # if not user: - # autoreg = galaxy.auth.check_auto_registration(trans, email, password, trans.app.config.auth_config_file) + # autoreg = galaxy.auth.check_auto_registration(trans, login, password, trans.app.config.auth_config_file) # if autoreg[0]: # kwd = {} - # kwd['username'] = autoreg[1] + # kwd['email'] = autoreg[1] + # kwd['username'] = autoreg[2] # params = util.Params( kwd ) - # message = validate_email( trans, email ) + # message = validate_email( trans, kwd['email'] ) # if not message: # message, status, user, success = self.__register( trans, 'user', False, **kwd ) # if success: diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index 47b83675efe..3d04a2351be 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -748,7 +748,7 @@ class PageController( BaseUIController, SharableMixin, # TODO: should be moved to history controller and/or called via ajax from the template decoded_id = self.decode_id( id ) # histories embedded in pages are set to importable when embedded, check for access here - history = self.history_manager.get_accessible( trans, decoded_id, trans.user ) + history = self.history_manager.get_accessible( decoded_id, trans.user, current_history=trans.history ) # create ownership flag for template, dictify models # note: adding original annotation since this is published - get_dict returns user-based annos @@ -798,8 +798,8 @@ class PageController( BaseUIController, SharableMixin, elif item_class == model.HistoryDatasetAssociation: decoded_id = self.decode_id( item_id ) - dataset = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) - dataset = self.hda_manager.error_if_uploading( trans, dataset ) + dataset = self.hda_manager.get_accessible( decoded_id, trans.user ) + dataset = self.hda_manager.error_if_uploading( dataset ) dataset.annotation = self.get_item_annotation_str( trans.sa_session, dataset.history.user, dataset ) if dataset: diff --git a/lib/galaxy/webapps/galaxy/controllers/user.py b/lib/galaxy/webapps/galaxy/controllers/user.py index 6077e340672..bea04c02c2e 100644 --- a/lib/galaxy/webapps/galaxy/controllers/user.py +++ b/lib/galaxy/webapps/galaxy/controllers/user.py @@ -14,7 +14,7 @@ from datetime import datetime, timedelta from galaxy import model from galaxy import util from galaxy import web -from galaxy.model.orm import and_ +from galaxy.model.orm import and_, or_ from galaxy.security.validate_user_input import (transform_publicname, validate_email, validate_password, @@ -467,7 +467,7 @@ class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin, Creat status = kwd.get( 'status', 'done' ) header = '' user = trans.user - email = kwd.get( 'email', '' ) + login = kwd.get( 'login', '' ) if user: # Already logged in. redirect_url = redirect @@ -496,7 +496,7 @@ class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin, Creat else: header = REQUIRE_LOGIN_TEMPLATE % ( "Galaxy tool shed", "" ) return trans.fill_template( '/user/login.mako', - email=email, + login=login, header=header, use_panels=use_panels, redirect_url=redirect_url, @@ -510,21 +510,23 @@ class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin, Creat def __validate_login( self, trans, **kwd ): """Validates numerous cases that might happen during the login time.""" - message = escape( kwd.get( 'message', '' ) ) status = kwd.get( 'status', 'error' ) - email = kwd.get( 'email', '' ) + login = kwd.get( 'login', '' ) password = kwd.get( 'password', '' ) - username = kwd.get( 'username', '' ) redirect = kwd.get( 'redirect', trans.request.referer ).strip() success = False - user = trans.sa_session.query( trans.app.model.User ).filter( trans.app.model.User.table.c.email == email ).first() + user = trans.sa_session.query( trans.app.model.User ).filter(or_( + trans.app.model.User.table.c.email == login, + trans.app.model.User.table.c.username == login + )).first() log.debug("trans.app.config.auth_config_file: %s" % trans.app.config.auth_config_file) if not user: - autoreg = trans.app.auth_manager.check_auto_registration(trans, email, password) + autoreg = trans.app.auth_manager.check_auto_registration(trans, login, password) if autoreg[0]: - kwd['username'] = autoreg[1] + kwd['email'] = autoreg[1] + kwd['username'] = autoreg[2] params = util.Params( kwd ) - message = validate_email( trans, email ) #self.__validate( trans, params, email, password, password, username ) + message = validate_email( trans, kwd['email'] ) #self.__validate( trans, params, email, password, password, username ) if not message: message, status, user, success = self.__register( trans, 'user', False, **kwd ) if success: @@ -552,11 +554,11 @@ class User( BaseUIController, UsesFormDefinitionsMixin, CreatesUsersMixin, Creat elif trans.app.config.user_activation_on and not user.active: # activation is ON and the user is INACTIVE if ( trans.app.config.activation_grace_period != 0 ): # grace period is ON if self.is_outside_grace_period( trans, user.create_time ): # User is outside the grace period. Login is disabled and he will have the activation email resent. - message, status = self.resend_verification_email( trans, email, username ) + message, status = self.resend_verification_email( trans, user.email, user.username ) else: # User is within the grace period, let him log in. message, success, status = self.proceed_login( trans, user, redirect ) else: # Grace period is off. Login is disabled and user will have the activation email resent. - message, status = self.resend_verification_email( trans, email, username ) + message, status = self.resend_verification_email( trans, user.email, user.username ) else: # activation is OFF message, success, status = self.proceed_login( trans, user, redirect ) return ( message, status, user, success ) diff --git a/lib/galaxy/webapps/galaxy/controllers/visualization.py b/lib/galaxy/webapps/galaxy/controllers/visualization.py index d1c9868939d..1d5da3d1a76 100644 --- a/lib/galaxy/webapps/galaxy/controllers/visualization.py +++ b/lib/galaxy/webapps/galaxy/controllers/visualization.py @@ -871,7 +871,7 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio dbkey = None if new_dataset_id: decoded_id = self.decode_id( new_dataset_id ) - hda = self.hda_manager.get_owned( trans, decoded_id, trans.user ) + hda = self.hda_manager.get_owned( decoded_id, trans.user, current_history=trans.user ) dbkey = hda.dbkey if dbkey == '?': dbkey = kwargs.get( "dbkey", None ) @@ -981,11 +981,11 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio viz = self.get_visualization( trans, id ) viz_config = self.get_visualization_config( trans, viz ) decoded_id = self.decode_id( viz_config[ 'dataset_id' ] ) - dataset = self.hda_manager.get_owned( trans, decoded_id, trans.user ) + dataset = self.hda_manager.get_owned( decoded_id, trans.user, current_history=trans.history ) else: # Loading new visualization. dataset = self.get_hda_or_ldda( trans, hda_ldda, dataset_id ) - job = self.hda_manager.creating_job( trans, dataset ) + job = self.hda_manager.creating_job( dataset ) viz_config = { 'dataset_id': dataset_id, 'tool_id': job.tool_id, @@ -1016,8 +1016,8 @@ class VisualizationController( BaseUIController, SharableMixin, UsesVisualizatio # get the hda if we can, then its data using the phyloviz parsers if dataset_id: decoded_id = self.decode_id( dataset_id ) - hda = self.hda_manager.get_accessible( trans, decoded_id, trans.user ) - hda = self.hda_manager.error_if_uploading( trans, hda ) + hda = self.hda_manager.get_accessible( decoded_id, trans.user ) + hda = self.hda_manager.error_if_uploading( hda ) else: return trans.show_message( "Phyloviz couldn't find a dataset_id" ) diff --git a/lib/galaxy/webapps/reports/buildapp.py b/lib/galaxy/webapps/reports/buildapp.py index b5fc37284b8..1b1965706c6 100644 --- a/lib/galaxy/webapps/reports/buildapp.py +++ b/lib/galaxy/webapps/reports/buildapp.py @@ -70,12 +70,13 @@ def app_factory( global_conf, **kwargs ): webapp = wrap_in_static( webapp, global_conf, **kwargs ) # Close any pooled database connections before forking try: - galaxy.model.mapping.metadata.engine.connection_provider._pool.dispose() + galaxy.model.mapping.metadata.bind.dispose() except: - pass + log.exception("Unable to dispose of pooled galaxy model database connections.") # Return return webapp + def wrap_in_middleware( app, global_conf, **local_conf ): """Based on the configuration wrap `app` in a set of common and useful middleware.""" # Merge the global and local configurations diff --git a/lib/galaxy/webapps/tool_shed/api/repositories.py b/lib/galaxy/webapps/tool_shed/api/repositories.py index 92fb87f519f..c92070157ef 100644 --- a/lib/galaxy/webapps/tool_shed/api/repositories.py +++ b/lib/galaxy/webapps/tool_shed/api/repositories.py @@ -12,6 +12,7 @@ from galaxy import util from galaxy import web from galaxy.exceptions import RequestParameterMissingException from galaxy.exceptions import RequestParameterInvalidException +from galaxy.exceptions import InsufficientPermissionsException from galaxy.exceptions import ActionInputError from galaxy.exceptions import ObjectNotFound from galaxy.exceptions import MalformedId diff --git a/lib/galaxy/webapps/tool_shed/api/search.py b/lib/galaxy/webapps/tool_shed/api/search.py index f6d6a152d98..c0db8897b90 100644 --- a/lib/galaxy/webapps/tool_shed/api/search.py +++ b/lib/galaxy/webapps/tool_shed/api/search.py @@ -22,12 +22,20 @@ class SearchController ( BaseAPIController ): TS config option toolshed_search_on has to be turned on and toolshed_whoosh_index_dir has to be specified and existing. - :param search_term: - :param page: - :param jsonp: - :param callback: + :param search_term: (required)the term to search + :type search_term: str - :returns dict: + :param page: (optional)requested page of the search + :type page: int + + :param jsonp: (optional)flag whether to use jsonp format response, defaults to False + :type jsonp: bool + + :param callback: (optional)name of the function to wrap callback in + used only when jsonp is true, defaults to 'callback' + :type callback: str + + :returns dict: object containing list of results and a hostname """ if not self.app.config.toolshed_search_on: raise exceptions.ConfigDoesNotAllowException( 'Searching the TS through the API is turned off for this instance.' ) @@ -46,7 +54,7 @@ class SearchController ( BaseAPIController ): results[ 'hostname' ] = url_for( '/', qualified = True ) if return_jsonp: - response = '%s(%s);' % ( callback, json.dumps( results ) ) + response = str( '%s(%s);' % ( callback, json.dumps( results ) ) ) else: response = json.dumps( results ) return response diff --git a/lib/galaxy/webapps/tool_shed/buildapp.py b/lib/galaxy/webapps/tool_shed/buildapp.py index 2f9e191b94b..dfe3dccce10 100644 --- a/lib/galaxy/webapps/tool_shed/buildapp.py +++ b/lib/galaxy/webapps/tool_shed/buildapp.py @@ -163,9 +163,9 @@ def app_factory( global_conf, **kwargs ): webapp = wrap_in_static( webapp, global_conf, **kwargs ) # Close any pooled database connections before forking try: - galaxy.webapps.tool_shed.model.mapping.metadata.engine.connection_provider._pool.dispose() + galaxy.webapps.tool_shed.model.mapping.metadata.bind.dispose() except: - pass + log.exception("Unable to dispose of pooled tool_shed model database connections.") # Return return webapp diff --git a/lib/galaxy/workflow/run_request.py b/lib/galaxy/workflow/run_request.py index 3def2dcb6e3..260eb29a622 100644 --- a/lib/galaxy/workflow/run_request.py +++ b/lib/galaxy/workflow/run_request.py @@ -196,7 +196,7 @@ def build_workflow_run_config( trans, workflow, payload ): # Passing an existing history to use. encoded_history_id = history_param[ 8: ] history_id = __decode_id( trans, encoded_history_id, model_type="history" ) - history = history_manager.get_owned( trans, history_id, trans.user ) + history = history_manager.get_owned( history_id, trans.user, current_history=trans.history ) else: # Send workflow outputs to new history. history = app.model.History(name=history_param, user=trans.user) diff --git a/lib/pulsar/client/amqp_exchange.py b/lib/pulsar/client/amqp_exchange.py index 896efd19f96..ae22c82f5af 100644 --- a/lib/pulsar/client/amqp_exchange.py +++ b/lib/pulsar/client/amqp_exchange.py @@ -83,7 +83,7 @@ class PulsarExchange(object): connection.drain_events(timeout=self.__timeout) except socket.timeout: pass - except (IOError, socket.error), exc: + except (IOError, socket.error) as exc: self.__handle_io_error(exc, heartbeat_thread) except BaseException: log.exception("Problem consuming queue, consumer quitting in problematic fashion!") diff --git a/lib/pulsar/client/client.py b/lib/pulsar/client/client.py index cbd755e0aed..27bb22ab37b 100644 --- a/lib/pulsar/client/client.py +++ b/lib/pulsar/client/client.py @@ -1,5 +1,6 @@ import os from json import dumps +from json import loads from .destination import submit_params from .setup_handler import build as build_setup_handler @@ -170,10 +171,13 @@ class JobClient(BaseJobClient): input_path = path if contents: input_path = None - if action_type == 'transfer': + # action type == 'message' should either copy or transfer + # depending on default not just fallback to transfer. + if action_type in ['transfer', 'message']: return self._upload_file(args, contents, input_path) elif action_type == 'copy': - pulsar_path = self._raw_execute('path', args) + path_response = self._raw_execute('path', args) + pulsar_path = loads(path_response)['path'] copy(path, pulsar_path) return {'path': pulsar_path} diff --git a/lib/pulsar/client/staging/up.py b/lib/pulsar/client/staging/up.py index 3daf124085e..6fd01136aca 100644 --- a/lib/pulsar/client/staging/up.py +++ b/lib/pulsar/client/staging/up.py @@ -348,7 +348,7 @@ class TransferTracker(object): if action.staging_needed: local_action = action.staging_action_local if local_action: - response = self.client.put_file(path, type, name=name, contents=contents) + response = self.client.put_file(path, type, name=name, contents=contents, action_type=action.action_type) def get_path(): return response['path'] diff --git a/lib/tool_shed/galaxy_install/tool_dependencies/recipe/install_environment.py b/lib/tool_shed/galaxy_install/tool_dependencies/recipe/install_environment.py index 1c9c132b802..d336c711f42 100644 --- a/lib/tool_shed/galaxy_install/tool_dependencies/recipe/install_environment.py +++ b/lib/tool_shed/galaxy_install/tool_dependencies/recipe/install_environment.py @@ -46,11 +46,6 @@ class InstallEnvironment( object ): self.install_dir = install_dir self.tool_shed_repository_install_dir = tool_shed_repository_install_dir - def __call__( self ): - with settings( warn_only=True, **basic_util.get_env_var_values( self ) ): - with prefix( self.__setup_environment() ): - yield - def add_env_shell_file_paths( self, paths ): for path in paths: self.env_shell_file_paths.append( str( path ) ) diff --git a/lib/tool_shed/galaxy_install/tool_dependencies/recipe/step_handler.py b/lib/tool_shed/galaxy_install/tool_dependencies/recipe/step_handler.py index 4db6d3efb12..5d469b90c50 100644 --- a/lib/tool_shed/galaxy_install/tool_dependencies/recipe/step_handler.py +++ b/lib/tool_shed/galaxy_install/tool_dependencies/recipe/step_handler.py @@ -238,7 +238,7 @@ class AssertDirectoryExecutable( RecipeStep ): return False if os.path.isdir( full_path ): # Make sure the owner has execute permission on the directory. - # See http://docs.python.org/2/library/stat.html + # See https://docs.python.org/2/library/stat.html if stat.S_IXUSR & os.stat( full_path )[ stat.ST_MODE ] == 64: return True return False @@ -334,7 +334,7 @@ class AssertFileExecutable( RecipeStep ): return False if os.path.exists( full_path ): # Make sure the owner has execute permission on the file. - # See http://docs.python.org/2/library/stat.html + # See https://docs.python.org/2/library/stat.html if stat.S_IXUSR & os.stat( full_path )[ stat.ST_MODE ] == 64: return True return False @@ -896,7 +896,7 @@ class SetEnvironment( RecipeStep ): in the tool_dependencies.xml file in the order discussed here. The example for this discussion is the tool_dependencies.xml file contained in the osra repository, which is available at: - http://testtoolshed.g2.bx.psu.edu/view/bgruening/osra + https://testtoolshed.g2.bx.psu.edu/view/bgruening/osra The first tag set defines a complex repository dependency like this. This tag set ensures that changeset revision XXX of the repository named package_graphicsmagick_1_3 owned by YYY in the tool shed ZZZ has been diff --git a/lib/tool_shed/scripts/api/export.py b/lib/tool_shed/scripts/api/export.py index dea6c5dd211..bfb6c17c74a 100644 --- a/lib/tool_shed/scripts/api/export.py +++ b/lib/tool_shed/scripts/api/export.py @@ -25,7 +25,7 @@ def generate_repository_archive_filename( tool_shed_url, name, owner, changeset_ export_repository_dependencies, use_tmp_archive_dir=False ): tool_shed = remove_protocol_from_tool_shed_url( tool_shed_url ) file_type_str = basic_util.get_file_type_str( changeset_revision, file_type ) - if self.export_repository_dependencies: + if export_repository_dependencies: repositories_archive_filename = '%s_%s_%s_%s_%s' % ( CAPSULE_WITH_DEPENDENCIES_FILENAME, tool_shed, name, diff --git a/lib/tool_shed/scripts/api/import_capsule.py b/lib/tool_shed/scripts/api/import_capsule.py index 3f9bce80351..e58cc25df82 100644 --- a/lib/tool_shed/scripts/api/import_capsule.py +++ b/lib/tool_shed/scripts/api/import_capsule.py @@ -14,7 +14,8 @@ method (they'll have to be created manually, which can be done after the import) Here is a working example of how to use this script to install a repository from the test tool shed. ./import_capsule.py -a -u http://localhost:9009 -c capsule_localhost_colon_9009_filter_test1_8923f52d5c6d.tar.gz """ - +import logging +log = logging.getLogger(__name__) import os import sys import argparse diff --git a/lib/tool_shed/scripts/api/remove_repository_registry_entry.py b/lib/tool_shed/scripts/api/remove_repository_registry_entry.py index 5eec6ecfe11..730b3262693 100644 --- a/lib/tool_shed/scripts/api/remove_repository_registry_entry.py +++ b/lib/tool_shed/scripts/api/remove_repository_registry_entry.py @@ -26,7 +26,7 @@ def main( options ): response_dict = submit( url, data, api_key=api_key, return_formatted=False ) print response_dict else: - print "Invalid tool_shed: ", base_tool_shed_url, " name: ", name, " or owner: ", owner, "." + print "Invalid tool_shed: ", base_tool_shed_url, " name: ", options.name, " or owner: ", options.owner, "." else: print "An API key for an admin user in the Tool Shed is required to remove entries from the Tool Shed's repository registry." diff --git a/lib/tool_shed/scripts/api/reset_metadata_on_repositories.py b/lib/tool_shed/scripts/api/reset_metadata_on_repositories.py index 104689e87ac..fe3a1fcf64e 100644 --- a/lib/tool_shed/scripts/api/reset_metadata_on_repositories.py +++ b/lib/tool_shed/scripts/api/reset_metadata_on_repositories.py @@ -11,7 +11,8 @@ usage: reset_metadata_on_repositories.py key Here is a working example of how to use this script to reset metadata on certain repositories in a specified Tool Shed. python ./reset_metadata_on_repositories.py -a 22be3b -m True -u http://localhost:9009/ """ - +import logging +log = logging.getLogger(__name__) import argparse import os import sys diff --git a/lib/tool_shed/util/common_util.py b/lib/tool_shed/util/common_util.py index be33e737f9d..caa70774cc8 100644 --- a/lib/tool_shed/util/common_util.py +++ b/lib/tool_shed/util/common_util.py @@ -334,7 +334,7 @@ def tool_shed_get( app, tool_shed_url, uri ): """Make contact with the tool shed via the uri provided.""" registry = app.tool_shed_registry # urllib2 auto-detects system proxies, when passed a Proxyhandler. - # Refer: http://docs.python.org/2/howto/urllib2.html#proxies + # Refer: https://docs.python.org/2/howto/urllib2.html#proxies proxy = urllib2.ProxyHandler() urlopener = urllib2.build_opener( proxy ) urllib2.install_opener( urlopener ) diff --git a/lib/tool_shed/util/tool_util.py b/lib/tool_shed/util/tool_util.py index 2f907cf4014..10fec0e2f5c 100644 --- a/lib/tool_shed/util/tool_util.py +++ b/lib/tool_shed/util/tool_util.py @@ -237,12 +237,19 @@ def new_state( trans, tool, invalid=False ): if invalid: # We're attempting to display a tool in the tool shed that has been determined to have errors, so is invalid. return state + try: + # Attempt to generate the tool state using the standard Galaxy-side code + return tool.new_state( trans ) + except Exception, e: + # Fall back to building tool state as below + log.debug( 'Failed to build tool state for tool "%s" using standard method, will try to fall back on custom method: %s', tool.id, e ) inputs = tool.inputs_by_page[ 0 ] context = ExpressionContext( state.inputs, parent=None ) for input in inputs.itervalues(): try: state.inputs[ input.name ] = input.get_initial_value( trans, context ) except: + # FIXME: not all values should be an empty list state.inputs[ input.name ] = [] return state diff --git a/run_tests.sh b/run_tests.sh index 8b5dc93f61a..02edf48ca22 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -3,29 +3,40 @@ ./scripts/common_startup.sh # A good place to look for nose info: http://somethingaboutorange.com/mrl/projects/nose/ -rm -f run_functional_tests.log +rm -f run_functional_tests.log show_help() { cat <0},fetch:function(a){var b=this;return Backbone.Model.prototype.fetch.call(this,a).always(function(){b._generateUrls()})},"delete":function(a){return this.get("deleted")?jQuery.when():this.save({deleted:!0},a)},undelete:function(a){return!this.get("deleted")||this.get("purged")?jQuery.when():this.save({deleted:!1},a)},purge:function(a){if(this.get("purged"))return jQuery.when();a=a||{},a.url=this.urls.purge;var b=this,d=jQuery.ajax(a);return d.done(function(){b.set({deleted:!0,purged:!0})}),d.fail(function(d){var e=c("Unable to purge dataset"),f="Removal of datasets by users is not allowed in this Galaxy instance";d.responseJSON&&d.responseJSON.error?e=d.responseJSON.error:-1!==d.responseText.indexOf(f)&&(e=f),d.responseText=e,b.trigger("error",b,d,a,c(e),{error:e})}),d},searchAttributes:["name","file_ext","genome_build","misc_blurb","misc_info","annotation","tags"],searchAliases:{title:"name",format:"file_ext",database:"genome_build",blurb:"misc_blurb",description:"misc_blurb",info:"misc_info",tag:"tags"},toString:function(){var a=this.get("id")||"";return this.get("name")&&(a='"'+this.get("name")+'",'+a),"Dataset("+a+")"}})),f=Backbone.Collection.extend(b.LoggableMixin).extend({model:e,urlRoot:(window.galaxy_config&&galaxy_config.root?galaxy_config.root:"/")+"api/datasets",ids:function(){return this.map(function(a){return a.get("id")})},notReady:function(){return this.filter(function(a){return!a.inReadyState()})},haveDetails:function(){return this.all(function(a){return a.hasDetails()})},ajaxQueue:function(a,b){var c=jQuery.Deferred(),d=this.length,e=[];if(!d)return c.resolve([]),c;var f=this.chain().reverse().map(function(g,h){return function(){var i=a.call(g,b);i.done(function(a){c.notify({curr:h,total:d,response:a,model:g})}),i.always(function(a){e.push(a),f.length?f.shift()():c.resolve(e)})}}).value();return f.shift()(),c},matches:function(a){return this.filter(function(b){return b.matches(a)})},toString:function(){return["DatasetAssociationCollection(",this.length,")"].join("")}});return{DatasetAssociation:e,DatasetAssociationCollection:f}}); +define(["mvc/dataset/states","mvc/base-mvc","utils/localization"],function(a,b,c){var d=b.SearchableModelMixin,e=Backbone.Model.extend(b.LoggableMixin).extend(b.mixin(d,{defaults:{state:a.NEW,deleted:!1,purged:!1,name:"(unnamed dataset)",accessible:!0,data_type:"",file_ext:"",file_size:0,meta_files:[],misc_blurb:"",misc_info:"",tags:[]},initialize:function(b,c){this.debug(this+"(Dataset).initialize",b,c),this.get("accessible")||this.set("state",a.NOT_VIEWABLE),this.urls=this._generateUrls(),this._setUpListeners()},_generateUrls:function(){var a=this.get("id");if(!a)return{};var b={purge:"datasets/"+a+"/purge_async",display:"datasets/"+a+"/display/?preview=True",edit:"datasets/"+a+"/edit",download:"datasets/"+a+"/display?to_ext="+this.get("file_ext"),report_error:"dataset/errors?id="+a,rerun:"tool_runner/rerun?id="+a,show_params:"datasets/"+a+"/show_params",visualization:"visualization",meta_download:"dataset/get_metadata_file?hda_id="+a+"&metadata_name="},c=window.galaxy_config&&galaxy_config.root?galaxy_config.root:"/";return _.each(b,function(a,d){b[d]=c+a}),this.urls=b,b},_setUpListeners:function(){this.on("change:state",function(a,b){this.log(this+" has changed state:",a,b),this.inReadyState()&&this.trigger("state:ready",a,b,this.previous("state"))}),this.on("change:id change:file_ext",function(){this._generateUrls()})},toJSON:function(){var a=Backbone.Model.prototype.toJSON.call(this);return _.extend(a,{urls:this.urls})},isDeletedOrPurged:function(){return this.get("deleted")||this.get("purged")},inReadyState:function(){var b=_.contains(a.READY_STATES,this.get("state"));return this.isDeletedOrPurged()||b},hasDetails:function(){return _.has(this.attributes,"genome_build")},hasData:function(){return this.get("file_size")>0},fetch:function(a){var b=this;return Backbone.Model.prototype.fetch.call(this,a).always(function(){b._generateUrls()})},"delete":function(a){return this.get("deleted")?jQuery.when():this.save({deleted:!0},a)},undelete:function(a){return!this.get("deleted")||this.get("purged")?jQuery.when():this.save({deleted:!1},a)},purge:function(a){if(this.get("purged"))return jQuery.when();a=a||{},a.url=this.urls.purge;var b=this,d=jQuery.ajax(a);return d.done(function(){b.set({deleted:!0,purged:!0})}),d.fail(function(d){var e=c("Unable to purge dataset"),f="Removal of datasets by users is not allowed in this Galaxy instance";d.responseJSON&&d.responseJSON.error?e=d.responseJSON.error:-1!==d.responseText.indexOf(f)&&(e=f),d.responseText=e,b.trigger("error",b,d,a,c(e),{error:e})}),d},searchAttributes:["name","file_ext","genome_build","misc_blurb","misc_info","annotation","tags"],searchAliases:{title:"name",format:"file_ext",database:"genome_build",blurb:"misc_blurb",description:"misc_blurb",info:"misc_info",tag:"tags"},toString:function(){var a=this.get("id")||"";return this.get("name")&&(a='"'+this.get("name")+'",'+a),"Dataset("+a+")"}})),f=Backbone.Collection.extend(b.LoggableMixin).extend({model:e,urlRoot:(window.galaxy_config&&galaxy_config.root?galaxy_config.root:"/")+"api/datasets",url:function(){return this.urlRoot},ids:function(){return this.map(function(a){return a.get("id")})},notReady:function(){return this.filter(function(a){return!a.inReadyState()})},haveDetails:function(){return this.all(function(a){return a.hasDetails()})},ajaxQueue:function(a,b){var c=jQuery.Deferred(),d=this.length,e=[];if(!d)return c.resolve([]),c;var f=this.chain().reverse().map(function(g,h){return function(){var i=a.call(g,b);i.done(function(a){c.notify({curr:h,total:d,response:a,model:g})}),i.always(function(a){e.push(a),f.length?f.shift()():c.resolve(e)})}}).value();return f.shift()(),c},matches:function(a){return this.filter(function(b){return b.matches(a)})},toString:function(){return["DatasetAssociationCollection(",this.length,")"].join("")}});return{DatasetAssociation:e,DatasetAssociationCollection:f}}); //# sourceMappingURL=../../../maps/mvc/dataset/dataset-model.js.map \ No newline at end of file diff --git a/static/scripts/mvc/form/form-data.js b/static/scripts/mvc/form/form-data.js index 79473f6f824..3e74c8c30a6 100644 --- a/static/scripts/mvc/form/form-data.js +++ b/static/scripts/mvc/form/form-data.js @@ -1,2 +1,2 @@ -define(["utils/utils"],function(){return Backbone.Model.extend({initialize:function(a){this.app=a},checksum:function(){var a="";for(var b in this.app.field_list){var c=this.app.field_list[b];a+=JSON.stringify(c.value&&c.value())}return a},create:function(){function a(a,b,d){c.map_dict[a]=b,e[a]=d}function b(d,e){for(var f in e){var g=e[f];if(g.input){var h=g.input,i=d;switch(""!=d&&(i+="|"),i+=h.name,h.type){case"repeat":var j="section-",k=[],l=null;for(var m in g){var n=m.indexOf(j);-1!=n&&(n+=j.length,k.push(parseInt(m.substr(n))),l||(l=m.substr(0,n)))}k.sort(function(a,b){return a-b});var f=0;for(var o in k)b(i+"_"+f++,g[l+k[o]]);break;case"conditional":var p=c.app.field_list[h.id].value();a(i+"|"+h.test_param.name,h.id,p);var q=c.matchCase(h,p);-1!=q&&b(i,e[h.id+"-section-"+q]);break;case"section":b("",g);break;default:var r=c.app.field_list[h.id];if(r&&r.value){var p=r.value();if((void 0===h.ignore||h.ignore!=p)&&(a(i,h.id,p),h.payload))for(var s in h.payload)a(s,h.id,h.payload[s])}}}}}var c=this,d={};this._iterate(this.app.section.$el,d);var e={};return this.map_dict={},b("",d),e},match:function(a){return this.map_dict&&this.map_dict[a]},matchCase:function(a,b){"boolean"==a.test_param.type&&(b="true"==b?a.test_param.truevalue||"true":a.test_param.falsevalue||"false");for(var c in a.cases)if(a.cases[c].value==b)return c;return-1},matchModel:function(a,b){function c(a,d){for(var f in d){var g=d[f],h=g.name;switch(""!=a&&(h=a+"|"+h),g.type){case"repeat":for(var i in g.cache)c(h+"_"+i,g.cache[i]);break;case"conditional":var j=g.test_param&&g.test_param.value,k=e.matchCase(g,j);-1!=k&&c(h,g.cases[k].inputs);break;default:var l=e.map_dict[h];l&&b(l,g)}}}var d={},e=this;return c("",a.inputs),d},matchResponse:function(a){function b(a,e){if("string"==typeof e){var f=d.map_dict[a];f&&(c[f]=e)}else for(var g in e){var h=g;if(""!==a){var i="|";e instanceof Array&&(i="_"),h=a+i+h}b(h,e[g])}}var c={},d=this;return b("",a),c},_iterate:function(a,b){var c=this,d=$(a).children();d.each(function(){var a=this,d=$(a).attr("id");if($(a).hasClass("section-row")){b[d]={};var e=c.app.input_list[d];e&&(b[d]={input:e}),c._iterate(a,b[d])}else c._iterate(a,b)})}})}); +define(["utils/utils"],function(){return Backbone.Model.extend({initialize:function(a){this.app=a},checksum:function(){var a="";for(var b in this.app.field_list){var c=this.app.field_list[b];a+=JSON.stringify(c.value&&c.value())}return a},create:function(){function a(a,b,d){c.map_dict[a]=b,e[a]=d}function b(d,e){for(var f in e){var g=e[f];if(g.input){var h=g.input,i=d;switch(""!=d&&(i+="|"),i+=h.name,h.type){case"repeat":var j="section-",k=[],l=null;for(var m in g){var n=m.indexOf(j);-1!=n&&(n+=j.length,k.push(parseInt(m.substr(n))),l||(l=m.substr(0,n)))}k.sort(function(a,b){return a-b});var f=0;for(var o in k)b(i+"_"+f++,g[l+k[o]]);break;case"conditional":var p=c.app.field_list[h.id].value();a(i+"|"+h.test_param.name,h.id,p);var q=c.matchCase(h,p);-1!=q&&b(i,e[h.id+"-section-"+q]);break;case"section":b(i,g);break;default:var r=c.app.field_list[h.id];if(r&&r.value){var p=r.value();if((void 0===h.ignore||h.ignore!=p)&&(a(i,h.id,p),h.payload))for(var s in h.payload)a(s,h.id,h.payload[s])}}}}}var c=this,d={};this._iterate(this.app.section.$el,d);var e={};return this.map_dict={},b("",d),e},match:function(a){return this.map_dict&&this.map_dict[a]},matchCase:function(a,b){"boolean"==a.test_param.type&&(b="true"==b?a.test_param.truevalue||"true":a.test_param.falsevalue||"false");for(var c in a.cases)if(a.cases[c].value==b)return c;return-1},matchModel:function(a,b){function c(a,d){for(var f in d){var g=d[f],h=g.name;switch(""!=a&&(h=a+"|"+h),g.type){case"repeat":for(var i in g.cache)c(h+"_"+i,g.cache[i]);break;case"conditional":var j=g.test_param&&g.test_param.value,k=e.matchCase(g,j);-1!=k&&c(h,g.cases[k].inputs);break;case"section":c(h,g.inputs);break;default:var l=e.map_dict[h];l&&b(l,g)}}}var d={},e=this;return c("",a.inputs),d},matchResponse:function(a){function b(a,e){if("string"==typeof e){var f=d.map_dict[a];f&&(c[f]=e)}else for(var g in e){var h=g;if(""!==a){var i="|";e instanceof Array&&(i="_"),h=a+i+h}b(h,e[g])}}var c={},d=this;return b("",a),c},_iterate:function(a,b){var c=this,d=$(a).children();d.each(function(){var a=this,d=$(a).attr("id");if($(a).hasClass("section-row")){b[d]={};var e=c.app.input_list[d];e&&(b[d]={input:e}),c._iterate(a,b[d])}else c._iterate(a,b)})}})}); //# sourceMappingURL=../../../maps/mvc/form/form-data.js.map \ No newline at end of file diff --git a/static/scripts/mvc/form/form-section.js b/static/scripts/mvc/form/form-section.js index f6549141b0b..befc9ba0519 100644 --- a/static/scripts/mvc/form/form-section.js +++ b/static/scripts/mvc/form/form-section.js @@ -1,2 +1,2 @@ -define(["utils/utils","mvc/ui/ui-table","mvc/ui/ui-misc","mvc/ui/ui-portlet","mvc/form/form-repeat","mvc/form/form-input","mvc/form/form-parameters"],function(a,b,c,d,e,f,g){var h=Backbone.View.extend({initialize:function(a,c){this.app=a,this.inputs=c.inputs,c.cls="ui-table-plain",c.cls_tr="section-row",this.table=new b.View(c),this.parameters=new g(a,c),this.setElement(this.table.$el),this.render()},render:function(){this.table.delAll();for(var a in this.inputs)this.add(this.inputs[a])},add:function(b){var c=jQuery.extend(!0,{},b);c.id=b.id=a.uid(),this.app.input_list[c.id]=c;var d=c.type;switch(d){case"conditional":this._addConditional(c);break;case"repeat":this._addRepeat(c);break;case"section":this._addSection(c);break;default:this._addRow(c)}},_addConditional:function(a){var b=this;a.test_param.id=a.id;var c=this._addRow(a.test_param);c.options.onchange=function(c){var d=b.app.data.matchCase(a,c);for(var e in a.cases){var f=a.cases[e],g=a.id+"-section-"+e,h=b.table.get(g),i=!1;for(var j in f.inputs)if(!f.inputs[j].hidden){i=!0;break}e==d&&i?h.fadeIn("fast"):h.hide()}b.app.trigger("change")};for(var d in a.cases){var e=a.id+"-section-"+d,f=new h(this.app,{inputs:a.cases[d].inputs});f.$el.addClass("ui-table-section"),this.table.add(f.$el),this.table.append(e)}c.trigger("change")},_addRepeat:function(a){function b(b){var e=a.id+"-section-"+d++,f=new h(c.app,{inputs:b});g.add({id:e,$el:f.$el,ondel:function(){g.del(e),c.app.trigger("change")}})}for(var c=this,d=0,g=new e.View({title:a.title,title_new:a.title,min:a.min,max:a.max,onnew:function(){b(a.inputs),c.app.trigger("change")}}),i=a.min,j=_.size(a.cache),k=0;kk?a.cache[k]:a.inputs,b(l)}var m=new f(this.app,{label:a.title,help:a.help,field:g});this.table.add(m.$el),this.table.append(a.id)},_addSection:function(a){var b=this,e=new h(b.app,{inputs:a.inputs}),f=new c.ButtonIcon({icon:"fa-eye-slash",tooltip:"Show/hide section",cls:"ui-button-icon-plain"}),g=new d.View({title:a.title,cls:"ui-portlet-section",operations:{button_visible:f}});g.append(e.$el),g.append($("
").addClass("ui-table-form-info").html(a.help));var i=!1;g.$content.hide(),g.$header.css("cursor","pointer"),g.$header.on("click",function(){i?(i=!1,g.$content.hide(),f.setIcon("fa-eye-slash")):(i=!0,g.$content.fadeIn("fast"),f.setIcon("fa-eye"))}),a.expand&&g.$header.trigger("click"),this.table.add(g.$el),this.table.append(a.id)},_addRow:function(a){var b=a.id,c=this.parameters.create(a);this.app.field_list[b]=c;var d=new f(this.app,{label:a.label,default_value:a.default_value,optional:a.optional,help:a.help,field:c});return this.app.element_list[b]=d,this.table.add(d.$el),this.table.append(b),a.hidden&&this.table.get(b).hide(),c}});return{View:h}}); +define(["utils/utils","mvc/ui/ui-table","mvc/ui/ui-misc","mvc/ui/ui-portlet","mvc/form/form-repeat","mvc/form/form-input","mvc/form/form-parameters"],function(a,b,c,d,e,f,g){var h=Backbone.View.extend({initialize:function(a,c){this.app=a,this.inputs=c.inputs,c.cls="ui-table-plain",c.cls_tr="section-row",this.table=new b.View(c),this.parameters=new g(a,c),this.setElement(this.table.$el),this.render()},render:function(){this.table.delAll();for(var a in this.inputs)this.add(this.inputs[a])},add:function(b){var c=jQuery.extend(!0,{},b);c.id=b.id=a.uid(),this.app.input_list[c.id]=c;var d=c.type;switch(d){case"conditional":this._addConditional(c);break;case"repeat":this._addRepeat(c);break;case"section":this._addSection(c);break;default:this._addRow(c)}},_addConditional:function(a){var b=this;a.test_param.id=a.id;var c=this._addRow(a.test_param);c.options.onchange=function(c){var d=b.app.data.matchCase(a,c);for(var e in a.cases){var f=a.cases[e],g=a.id+"-section-"+e,h=b.table.get(g),i=!1;for(var j in f.inputs)if(!f.inputs[j].hidden){i=!0;break}e==d&&i?h.fadeIn("fast"):h.hide()}b.app.trigger("change")};for(var d in a.cases){var e=a.id+"-section-"+d,f=new h(this.app,{inputs:a.cases[d].inputs});f.$el.addClass("ui-table-section"),this.table.add(f.$el),this.table.append(e)}c.trigger("change")},_addRepeat:function(a){function b(b){var e=a.id+"-section-"+d++,f=new h(c.app,{inputs:b});g.add({id:e,$el:f.$el,ondel:function(){g.del(e),c.app.trigger("change")}})}for(var c=this,d=0,g=new e.View({title:a.title,title_new:a.title,min:a.min,max:a.max,onnew:function(){b(a.inputs),c.app.trigger("change")}}),i=a.min,j=_.size(a.cache),k=0;kk?a.cache[k]:a.inputs,b(l)}var m=new f(this.app,{label:a.title,help:a.help,field:g});this.table.add(m.$el),this.table.append(a.id)},_addSection:function(a){var b=this,e=new h(b.app,{inputs:a.inputs}),f=new c.ButtonIcon({icon:"fa-eye-slash",tooltip:"Show/hide section",cls:"ui-button-icon-plain"}),g=new d.View({title:a.title,cls:"ui-portlet-section",operations:{button_visible:f}});g.append(e.$el),g.append($("
").addClass("ui-table-form-info").html(a.help));var i=!1;g.$content.hide(),g.$header.css("cursor","pointer"),g.$header.on("click",function(){i?(i=!1,g.$content.hide(),f.setIcon("fa-eye-slash")):(i=!0,g.$content.fadeIn("fast"),f.setIcon("fa-eye"))}),a.expanded&&g.$header.trigger("click"),this.table.add(g.$el),this.table.append(a.id)},_addRow:function(a){var b=a.id,c=this.parameters.create(a);this.app.field_list[b]=c;var d=new f(this.app,{label:a.label,default_value:a.default_value,optional:a.optional,help:a.help,field:c});return this.app.element_list[b]=d,this.table.add(d.$el),this.table.append(b),a.hidden&&this.table.get(b).hide(),c}});return{View:h}}); //# sourceMappingURL=../../../maps/mvc/form/form-section.js.map \ No newline at end of file diff --git a/static/scripts/mvc/form/form-view.js b/static/scripts/mvc/form/form-view.js index 36b276146db..e145f2683e2 100644 --- a/static/scripts/mvc/form/form-view.js +++ b/static/scripts/mvc/form/form-view.js @@ -1,2 +1,2 @@ -define(["utils/utils","mvc/ui/ui-portlet","mvc/ui/ui-misc","mvc/form/form-section","mvc/form/form-data"],function(a,b,c,d,e){return Backbone.View.extend({initialize:function(b){this.optionsDefault={is_workflow:!1,narrow:!1,initial_errors:!1,cls:"ui-portlet-limited"},this.options=a.merge(b,this.optionsDefault),console.debug(this.options);var d=parent.Galaxy;this.modal=d&&d.modal?d.modal:new c.Modal.View,this.setElement("
"),this._build()},update:function(a){var b=this;this.data.matchModel(a,function(a,c){var d=b.input_list[a];if(d&&d.options&&!_.isEqual(d.options,c.options)){d.options=c.options;var e=b.field_list[a];if(e.update){var f=[];if(-1!=["data","data_collection","drill_down"].indexOf(d.type))f=d.options;else for(var g in c.options){var h=c.options[g];h.length>2&&f.push({label:h[0],value:h[1]})}e.update(f),e.trigger("change"),console.debug("Updating options for "+a)}}})},wait:function(a){for(var b in this.input_list){var c=this.field_list[b],d=this.input_list[b];d.is_dynamic&&c.wait&&c.unwait&&(a?c.wait():c.unwait())}},reciept:function(a){this.$el.empty(),this.$el.append(a)},highlight:function(a,b,c){var d=this.element_list[a];d&&(d.error(b||"Please verify this parameter."),c||$("html, body").animate({scrollTop:d.$el.offset().top-20},500))},errors:function(a){if(this.trigger("reset"),a&&a.errors){var b=this.data.matchResponse(a.errors);for(var c in this.element_list){{this.element_list[c]}b[c]&&this.highlight(c,b[c],!0)}}},_build:function(){var a=this;this.off("change"),this.off("reset"),this.field_list={},this.input_list={},this.element_list={},this.data=new e(this),this._renderForm(),this.data.create(),this.options.initial_errors&&this.errors(this.options);var b=this.data.checksum();this.on("change",function(){var c=a.data.checksum();c!=b&&(b=c,a.options.onchange&&a.options.onchange())}),this.on("reset",function(){for(var a in this.element_list)this.element_list[a].reset()})},_renderForm:function(){this.message=new c.Message,this.section=new d.View(this,{inputs:this.options.inputs}),this.portlet=new b.View({icon:"fa-wrench",title:this.options.title,cls:this.options.cls,operations:this.options.operations,buttons:this.options.buttons}),this.portlet.append(this.message.$el.addClass("ui-margin-top")),this.portlet.append(this.section.$el),this.$el.empty(),this.$el.append(this.portlet.$el),this.options.message&&this.message.update({persistent:!0,status:"warning",message:this.options.message}),console.debug("tools-form-base::initialize() - Completed.")}})}); +define(["utils/utils","mvc/ui/ui-portlet","mvc/ui/ui-misc","mvc/form/form-section","mvc/form/form-data"],function(a,b,c,d,e){return Backbone.View.extend({initialize:function(b){this.optionsDefault={is_workflow:!1,narrow:!1,initial_errors:!1,cls:"ui-portlet-limited"},this.options=a.merge(b,this.optionsDefault),console.debug(this.options);var d=parent.Galaxy;this.modal=d&&d.modal?d.modal:new c.Modal.View,this.setElement("
"),this._build()},update:function(a){var b=this;this.data.matchModel(a,function(a,c){var d=b.input_list[a];if(d&&d.options&&!_.isEqual(d.options,c.options)){d.options=c.options;var e=b.field_list[a];if(e.update){var f=[];if(-1!=["data","data_collection","drill_down"].indexOf(d.type))f=d.options;else for(var g in c.options){var h=c.options[g];h.length>2&&f.push({label:h[0],value:h[1]})}e.update(f),e.trigger("change"),console.debug("Updating options for "+a)}}})},wait:function(a){for(var b in this.input_list){var c=this.field_list[b],d=this.input_list[b];d.is_dynamic&&c.wait&&c.unwait&&(a?c.wait():c.unwait())}},reciept:function(a){this.$el.empty(),this.$el.append(a)},highlight:function(a,b,c){var d=this.element_list[a];d&&(d.error(b||"Please verify this parameter."),c||$("html, body").animate({scrollTop:d.$el.offset().top-20},500))},errors:function(a){if(this.trigger("reset"),a&&a.errors){var b=this.data.matchResponse(a.errors);for(var c in this.element_list){{this.element_list[c]}b[c]&&this.highlight(c,b[c],!0)}}},_build:function(){var a=this;this.off("change"),this.off("reset"),this.field_list={},this.input_list={},this.element_list={},this.data=new e(this),this._renderForm(),this.data.create(),this.options.initial_errors&&this.errors(this.options);var b=this.data.checksum();this.on("change",function(){var c=a.data.checksum();c!=b&&(b=c,a.options.onchange&&a.options.onchange())}),this.on("reset",function(){for(var a in this.element_list)this.element_list[a].reset()})},_renderForm:function(){return this.message=new c.Message,this.section=new d.View(this,{inputs:this.options.inputs}),this.incompatible?(this.$el.hide(),void $("#tool-form-classic").show()):(this.portlet=new b.View({icon:"fa-wrench",title:this.options.title,cls:this.options.cls,operations:this.options.operations,buttons:this.options.buttons}),this.portlet.append(this.message.$el.addClass("ui-margin-top")),this.portlet.append(this.section.$el),this.$el.empty(),this.$el.append(this.portlet.$el),this.options.message&&this.message.update({persistent:!0,status:"warning",message:this.options.message}),void console.debug("tools-form-base::initialize() - Completed."))}})}); //# sourceMappingURL=../../../maps/mvc/form/form-view.js.map \ No newline at end of file diff --git a/static/scripts/mvc/history/history-panel-edit.js b/static/scripts/mvc/history/history-panel-edit.js index c7149d46e26..1d10888a343 100644 --- a/static/scripts/mvc/history/history-panel-edit.js +++ b/static/scripts/mvc/history/history-panel-edit.js @@ -1,2 +1,2 @@ -define(["mvc/history/history-panel","mvc/history/history-contents","mvc/dataset/states","mvc/history/hda-model","mvc/history/hda-li-edit","mvc/history/hdca-li-edit","mvc/tags","mvc/annotations","mvc/collection/list-collection-creator","mvc/collection/pair-collection-creator","mvc/collection/list-of-pairs-collection-creator","ui/fa-icon-button","mvc/ui/popup-menu","utils/localization"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n){var o=a.HistoryPanel,p=o.extend({HDAViewClass:e.HDAListItemEdit,HDCAViewClass:f.HDCAListItemEdit,initialize:function(a){a=a||{},o.prototype.initialize.call(this,a),this.tagsEditor=null,this.annotationEditor=null,this.purgeAllowed=a.purgeAllowed||!1,this.annotationEditorShown=a.annotationEditorShown||!1,this.tagsEditorShown=a.tagsEditorShown||!1},_setUpListeners:function(){var a=this;o.prototype._setUpListeners.call(a),a.on("drop",function(b,c){a.dataDropped(c),a.dropTargetOff()}),a.on("view:attached view:removed",function(){a._renderCounts()},a)},_setUpCollectionListeners:function(){return o.prototype._setUpCollectionListeners.call(this),this.collection.on("change:deleted",this._handleHdaDeletionChange,this),this.collection.on("change:visible",this._handleHdaVisibleChange,this),this.collection.on("change:purged",function(){this.model.fetch()},this),this},_setUpModelListeners:function(){return o.prototype._setUpModelListeners.call(this),this.model.on("change:nice_size",this.updateHistoryDiskSize,this),this},_buildNewRender:function(){var a=o.prototype._buildNewRender.call(this);return this.model?(Galaxy&&Galaxy.currUser&&Galaxy.currUser.id&&Galaxy.currUser.id===this.model.get("user_id")&&(this._renderTags(a),this._renderAnnotation(a)),a):a},renderItems:function(a){var b=o.prototype.renderItems.call(this,a);return this._renderCounts(a),b},_renderCounts:function(a){function b(a,b){return['',b,""].join("")}a=a||this.$el;var c=this.collection.where({deleted:!0}),d=this.collection.where({visible:!1}),e=[];return this.views.length&&e.push([this.views.length,n("shown")].join(" ")),c.length&&e.push(this.showDeleted?b("toggle-deleted-link",n("hide deleted")):[c.length,b("toggle-deleted-link",n("deleted"))].join(" ")),d.length&&e.push(this.showHidden?b("toggle-hidden-link",n("hide hidden")):[d.length,b("toggle-hidden-link",n("hidden"))].join(" ")),a.find("> .controls .subtitle").html(e.join(", "))},_renderTags:function(a){var b=this;this.tagsEditor=new g.TagsEditor({model:this.model,el:a.find(".controls .tags-display"),onshowFirstTime:function(){this.render()},onshow:function(){b.toggleHDATagEditors(!0,b.fxSpeed)},onhide:function(){b.toggleHDATagEditors(!1,b.fxSpeed)},$activator:l({title:n("Edit history tags"),classes:"history-tag-btn",faIcon:"fa-tags"}).appendTo(a.find(".controls .actions"))})},_renderAnnotation:function(a){var b=this;this.annotationEditor=new h.AnnotationEditor({model:this.model,el:a.find(".controls .annotation-display"),onshowFirstTime:function(){this.render()},onshow:function(){b.toggleHDAAnnotationEditors(!0,b.fxSpeed)},onhide:function(){b.toggleHDAAnnotationEditors(!1,b.fxSpeed)},$activator:l({title:n("Edit history annotation"),classes:"history-annotate-btn",faIcon:"fa-comment"}).appendTo(a.find(".controls .actions"))})},_setUpBehaviors:function(a){if(a=a||this.$el,o.prototype._setUpBehaviors.call(this,a),this.model&&Galaxy.currUser&&!Galaxy.currUser.isAnonymous()&&Galaxy.currUser.id===this.model.get("user_id")){var b=this,c="> .controls .name";a.find(c).attr("title",n("Click to rename history")).tooltip({placement:"bottom"}).make_text_editable({on_finish:function(a){var d=b.model.get("name");a&&a!==d?(b.$el.find(c).text(a),b.model.save({name:a}).fail(function(){b.$el.find(c).text(b.model.previous("name"))})):b.$el.find(c).text(d)}})}},multiselectActions:function(){var a=this,b=[{html:n("Hide datasets"),func:function(){var b=d.HistoryDatasetAssociation.prototype.hide;a.getSelectedModels().ajaxQueue(b)}},{html:n("Unhide datasets"),func:function(){var b=d.HistoryDatasetAssociation.prototype.unhide;a.getSelectedModels().ajaxQueue(b)}},{html:n("Delete datasets"),func:function(){var b=d.HistoryDatasetAssociation.prototype.delete;a.getSelectedModels().ajaxQueue(b)}},{html:n("Undelete datasets"),func:function(){var b=d.HistoryDatasetAssociation.prototype.undelete;a.getSelectedModels().ajaxQueue(b)}}];return a.purgeAllowed&&b.push({html:n("Permanently delete datasets"),func:function(){if(confirm(n("This will permanently remove the data in your datasets. Are you sure?"))){var b=d.HistoryDatasetAssociation.prototype.purge;a.getSelectedModels().ajaxQueue(b)}}}),b=b.concat(a._collectionActions())},_collectionActions:function(){var a=this;return[{html:n("Build Dataset List"),func:function(){i.createListHDCA(a.getSelectedModels()).done(function(){a.model.refresh()})}},{html:n("Build Dataset Pair"),func:function(){j.createPairHDCA(a.getSelectedModels()).done(function(){a.model.refresh()})}},{html:n("Build List of Dataset Pairs"),func:function(){k.createListOfPairsCollection(a.getSelectedModels()).done(function(){a.model.refresh()})}}]},_attachItems:function(a){return this.$list(a).append(this.views.reverse().map(function(a){return a.$el})),this},_attachView:function(a){var b=this;b.views.unshift(a),b.$list().prepend(a.render(0).$el.hide()),b.trigger("view:attached",a),a.$el.slideDown(b.fxSpeed,function(){b.trigger("view:attached:rendered")})},_getItemViewOptions:function(a){var b=o.prototype._getItemViewOptions.call(this,a);return _.extend(b,{purgeAllowed:this.purgeAllowed,tagsEditorShown:this.tagsEditor&&!this.tagsEditor.hidden,annotationEditorShown:this.annotationEditor&&!this.annotationEditor.hidden}),b},_handleHdaDeletionChange:function(a){a.get("deleted")&&!this.showDeleted&&this.removeItemView(a),this._renderCounts()},_handleHdaVisibleChange:function(a){a.hidden()&&!this.storage.showHidden&&this.removeItemView(a),this._renderCounts()},toggleHDATagEditors:function(){var a=Array.prototype.slice.call(arguments,1);_.each(this.views,function(b){b.tagsEditor&&b.tagsEditor.toggle.apply(b.tagsEditor,a)})},toggleHDAAnnotationEditors:function(){var a=Array.prototype.slice.call(arguments,1);_.each(this.views,function(b){b.annotationEditor&&b.annotationEditor.toggle.apply(b.annotationEditor,a)})},events:_.extend(_.clone(o.prototype.events),{"click .show-selectors-btn":"toggleSelectors","click .toggle-deleted-link":function(){this.toggleShowDeleted()},"click .toggle-hidden-link":function(){this.toggleShowHidden()}}),updateHistoryDiskSize:function(){this.$el.find(".history-size").text(this.model.get("nice_size"))},dropTargetOn:function(){if(this.dropTarget)return this;this.dropTarget=!0;var a={dragenter:_.bind(this.dragenter,this),dragover:_.bind(this.dragover,this),dragleave:_.bind(this.dragleave,this),drop:_.bind(this.drop,this)},b=this._renderDropTarget();this.$list().before([this._renderDropTargetHelp(),b]);for(var c in a)a.hasOwnProperty(c)&&b.on(c,a[c]);return this},_renderDropTarget:function(){return this.$(".history-drop-target").remove(),$("
").addClass("history-drop-target").css({height:"64px",margin:"0px 10px 10px 10px",border:"1px dashed black","border-radius":"3px"})},_renderDropTargetHelp:function(){return this.$(".history-drop-target-help").remove(),$("
").addClass("history-drop-target-help").css({margin:"10px 10px 4px 10px",color:"grey","font-size":"80%","font-style":"italic"}).text(n("Drag datasets here to copy them to the current history"))},dropTargetOff:function(){if(!this.dropTarget)return this;this.dropTarget=!1;var a=this.$(".history-drop-target").get(0);for(var b in this._dropHandlers)this._dropHandlers.hasOwnProperty(b)&&(console.debug(b,this._dropHandlers[b]),a.off(b,this._dropHandlers[b]));return this.$(".history-drop-target").remove(),this.$(".history-drop-target-help").remove(),this},dropTargetToggle:function(){return this.dropTarget?this.dropTargetOff():this.dropTargetOn(),this},dragenter:function(a){a.preventDefault(),a.stopPropagation(),this.$(".history-drop-target").css("border","2px solid black")},dragover:function(a){a.preventDefault(),a.stopPropagation()},dragleave:function(a){a.preventDefault(),a.stopPropagation(),this.$(".history-drop-target").css("border","1px dashed black")},drop:function(a){a.preventDefault(),a.dataTransfer.dropEffect="move";var b=this,c=a.dataTransfer.getData("text");try{c=JSON.parse(c)}catch(d){this.warn("error parsing JSON from drop:",c)}return this.trigger("droptarget:drop",a,c,b),!1},dataDropped:function(a){var b=this;return _.isObject(a)&&"HistoryDatasetAssociation"===a.model_class&&a.id?b.model.contents.copy(a.id):jQuery.when()},toString:function(){return"HistoryPanelEdit("+(this.model?this.model.get("name"):"")+")"}});return{HistoryPanelEdit:p}}); +define(["mvc/history/history-panel","mvc/history/history-contents","mvc/dataset/states","mvc/history/hda-model","mvc/history/hda-li-edit","mvc/history/hdca-li-edit","mvc/tags","mvc/annotations","mvc/collection/list-collection-creator","mvc/collection/pair-collection-creator","mvc/collection/list-of-pairs-collection-creator","ui/fa-icon-button","mvc/ui/popup-menu","utils/localization"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n){var o=a.HistoryPanel,p=o.extend({HDAViewClass:e.HDAListItemEdit,HDCAViewClass:f.HDCAListItemEdit,initialize:function(a){a=a||{},o.prototype.initialize.call(this,a),this.tagsEditor=null,this.annotationEditor=null,this.purgeAllowed=a.purgeAllowed||!1,this.annotationEditorShown=a.annotationEditorShown||!1,this.tagsEditorShown=a.tagsEditorShown||!1},_setUpListeners:function(){var a=this;o.prototype._setUpListeners.call(a),a.on("drop",function(b,c){a.dataDropped(c),a.dropTargetOff()}),a.on("view:attached view:removed",function(){a._renderCounts()},a)},_setUpCollectionListeners:function(){return o.prototype._setUpCollectionListeners.call(this),this.collection.on("change:deleted",this._handleHdaDeletionChange,this),this.collection.on("change:visible",this._handleHdaVisibleChange,this),this.collection.on("change:purged",function(){this.model.fetch()},this),this},_setUpModelListeners:function(){return o.prototype._setUpModelListeners.call(this),this.model.on("change:nice_size",this.updateHistoryDiskSize,this),this},_buildNewRender:function(){var a=o.prototype._buildNewRender.call(this);return this.model?(Galaxy&&Galaxy.currUser&&Galaxy.currUser.id&&Galaxy.currUser.id===this.model.get("user_id")&&(this._renderTags(a),this._renderAnnotation(a)),a):a},renderItems:function(a){var b=o.prototype.renderItems.call(this,a);return this._renderCounts(a),b},_renderCounts:function(a){function b(a,b){return['',b,""].join("")}a=a||this.$el;var c=this.collection.where({deleted:!0}),d=this.collection.where({visible:!1}),e=[];return this.views.length&&e.push([this.views.length,n("shown")].join(" ")),c.length&&e.push(this.showDeleted?b("toggle-deleted-link",n("hide deleted")):[c.length,b("toggle-deleted-link",n("deleted"))].join(" ")),d.length&&e.push(this.showHidden?b("toggle-hidden-link",n("hide hidden")):[d.length,b("toggle-hidden-link",n("hidden"))].join(" ")),a.find("> .controls .subtitle").html(e.join(", "))},_renderTags:function(a){var b=this;this.tagsEditor=new g.TagsEditor({model:this.model,el:a.find(".controls .tags-display"),onshowFirstTime:function(){this.render()},onshow:function(){b.toggleHDATagEditors(!0,b.fxSpeed)},onhide:function(){b.toggleHDATagEditors(!1,b.fxSpeed)},$activator:l({title:n("Edit history tags"),classes:"history-tag-btn",faIcon:"fa-tags"}).appendTo(a.find(".controls .actions"))})},_renderAnnotation:function(a){var b=this;this.annotationEditor=new h.AnnotationEditor({model:this.model,el:a.find(".controls .annotation-display"),onshowFirstTime:function(){this.render()},onshow:function(){b.toggleHDAAnnotationEditors(!0,b.fxSpeed)},onhide:function(){b.toggleHDAAnnotationEditors(!1,b.fxSpeed)},$activator:l({title:n("Edit history annotation"),classes:"history-annotate-btn",faIcon:"fa-comment"}).appendTo(a.find(".controls .actions"))})},_setUpBehaviors:function(a){if(a=a||this.$el,o.prototype._setUpBehaviors.call(this,a),this.model&&Galaxy.currUser&&!Galaxy.currUser.isAnonymous()&&Galaxy.currUser.id===this.model.get("user_id")){var b=this,c="> .controls .name";a.find(c).attr("title",n("Click to rename history")).tooltip({placement:"bottom"}).make_text_editable({on_finish:function(a){var d=b.model.get("name");a&&a!==d?(b.$el.find(c).text(a),b.model.save({name:a}).fail(function(){b.$el.find(c).text(b.model.previous("name"))})):b.$el.find(c).text(d)}})}},multiselectActions:function(){var a=this,b=[{html:n("Hide datasets"),func:function(){var b=d.HistoryDatasetAssociation.prototype.hide;a.getSelectedModels().ajaxQueue(b)}},{html:n("Unhide datasets"),func:function(){var b=d.HistoryDatasetAssociation.prototype.unhide;a.getSelectedModels().ajaxQueue(b)}},{html:n("Delete datasets"),func:function(){var b=d.HistoryDatasetAssociation.prototype.delete;a.getSelectedModels().ajaxQueue(b)}},{html:n("Undelete datasets"),func:function(){var b=d.HistoryDatasetAssociation.prototype.undelete;a.getSelectedModels().ajaxQueue(b)}}];return a.purgeAllowed&&b.push({html:n("Permanently delete datasets"),func:function(){if(confirm(n("This will permanently remove the data in your datasets. Are you sure?"))){var b=d.HistoryDatasetAssociation.prototype.purge;a.getSelectedModels().ajaxQueue(b)}}}),b=b.concat(a._collectionActions())},_collectionActions:function(){var a=this;return[{html:n("Build Dataset List"),func:function(){i.createListHDCA(a.getSelectedModels()).done(function(){a.model.refresh()})}},{html:n("Build Dataset Pair"),func:function(){j.createPairHDCA(a.getSelectedModels()).done(function(){a.model.refresh()})}},{html:n("Build List of Dataset Pairs"),func:function(){k.createListOfPairsCollection(a.getSelectedModels()).done(function(){a.model.refresh()})}}]},_attachItems:function(a){return this.$list(a).append(this.views.reverse().map(function(a){return a.$el})),this},_attachView:function(a){var b=this;b.views.unshift(a),b.$list().prepend(a.render(0).$el.hide()),b.trigger("view:attached",a),a.$el.slideDown(b.fxSpeed,function(){b.trigger("view:attached:rendered")})},_getItemViewOptions:function(a){var b=o.prototype._getItemViewOptions.call(this,a);return _.extend(b,{purgeAllowed:this.purgeAllowed,tagsEditorShown:this.tagsEditor&&!this.tagsEditor.hidden,annotationEditorShown:this.annotationEditor&&!this.annotationEditor.hidden}),b},_handleHdaDeletionChange:function(a){a.get("deleted")&&!this.showDeleted&&this.removeItemView(a),this._renderCounts()},_handleHdaVisibleChange:function(a){a.hidden()&&!this.showHidden&&this.removeItemView(a),this._renderCounts()},toggleHDATagEditors:function(){var a=Array.prototype.slice.call(arguments,1);_.each(this.views,function(b){b.tagsEditor&&b.tagsEditor.toggle.apply(b.tagsEditor,a)})},toggleHDAAnnotationEditors:function(){var a=Array.prototype.slice.call(arguments,1);_.each(this.views,function(b){b.annotationEditor&&b.annotationEditor.toggle.apply(b.annotationEditor,a)})},events:_.extend(_.clone(o.prototype.events),{"click .show-selectors-btn":"toggleSelectors","click .toggle-deleted-link":function(){this.toggleShowDeleted()},"click .toggle-hidden-link":function(){this.toggleShowHidden()}}),updateHistoryDiskSize:function(){this.$el.find(".history-size").text(this.model.get("nice_size"))},dropTargetOn:function(){if(this.dropTarget)return this;this.dropTarget=!0;var a={dragenter:_.bind(this.dragenter,this),dragover:_.bind(this.dragover,this),dragleave:_.bind(this.dragleave,this),drop:_.bind(this.drop,this)},b=this._renderDropTarget();this.$list().before([this._renderDropTargetHelp(),b]);for(var c in a)a.hasOwnProperty(c)&&b.on(c,a[c]);return this},_renderDropTarget:function(){return this.$(".history-drop-target").remove(),$("
").addClass("history-drop-target").css({height:"64px",margin:"0px 10px 10px 10px",border:"1px dashed black","border-radius":"3px"})},_renderDropTargetHelp:function(){return this.$(".history-drop-target-help").remove(),$("
").addClass("history-drop-target-help").css({margin:"10px 10px 4px 10px",color:"grey","font-size":"80%","font-style":"italic"}).text(n("Drag datasets here to copy them to the current history"))},dropTargetOff:function(){if(!this.dropTarget)return this;this.dropTarget=!1;var a=this.$(".history-drop-target").get(0);for(var b in this._dropHandlers)this._dropHandlers.hasOwnProperty(b)&&(console.debug(b,this._dropHandlers[b]),a.off(b,this._dropHandlers[b]));return this.$(".history-drop-target").remove(),this.$(".history-drop-target-help").remove(),this},dropTargetToggle:function(){return this.dropTarget?this.dropTargetOff():this.dropTargetOn(),this},dragenter:function(a){a.preventDefault(),a.stopPropagation(),this.$(".history-drop-target").css("border","2px solid black")},dragover:function(a){a.preventDefault(),a.stopPropagation()},dragleave:function(a){a.preventDefault(),a.stopPropagation(),this.$(".history-drop-target").css("border","1px dashed black")},drop:function(a){a.preventDefault(),a.dataTransfer.dropEffect="move";var b=this,c=a.dataTransfer.getData("text");try{c=JSON.parse(c)}catch(d){this.warn("error parsing JSON from drop:",c)}return this.trigger("droptarget:drop",a,c,b),!1},dataDropped:function(a){var b=this;return _.isObject(a)&&"HistoryDatasetAssociation"===a.model_class&&a.id?b.model.contents.copy(a.id):jQuery.when()},toString:function(){return"HistoryPanelEdit("+(this.model?this.model.get("name"):"")+")"}});return{HistoryPanelEdit:p}}); //# sourceMappingURL=../../../maps/mvc/history/history-panel-edit.js.map \ No newline at end of file diff --git a/static/scripts/mvc/history/multi-panel.js b/static/scripts/mvc/history/multi-panel.js index d09120cb6f5..0ef96a512c7 100644 --- a/static/scripts/mvc/history/multi-panel.js +++ b/static/scripts/mvc/history/multi-panel.js @@ -1,2 +1,2 @@ -define(["mvc/history/history-model","mvc/history/history-panel-edit","mvc/base-mvc","utils/ajax-queue","ui/mode-button","ui/search-input"],function(a,b,c,d){function e(a,b){function c(a){if(!a){if(!Galaxy.modal.$("#invalid-title").size()){var b=$("

").attr("id","invalid-title").css({color:"red","margin-top":"8px"}).addClass("bg-danger").text(_l("Please enter a valid history title"));Galaxy.modal.$(".modal-body").append(b)}return!1}return a}function d(b){var c=$('

Copying history...

').css("margin-top","8px");Galaxy.modal.$(".modal-body").append(c),a.copy(!0,b).fail(function(){alert(_l("History could not be copied. Please contact a Galaxy administrator"))}).always(function(){Galaxy.modal.hide()})}function e(){var a=Galaxy.modal.$("#copy-modal-title").val();c(a)&&d(a)}if(b=b||{},!Galaxy||!Galaxy.modal)return a.copy();var f=a.get("name"),g="Copy of '"+f+"'";Galaxy.modal.show(_.extend({title:_l("Copying history")+' "'+f+'"',body:$(['
",''].join("")),buttons:{Cancel:function(){Galaxy.modal.hide()},Copy:e}},b)),$("#copy-modal-title").focus().select(),$("#copy-modal-title").on("keydown",function(a){13===a.keyCode&&e()})}var f=Backbone.View.extend(c.LoggableMixin).extend({tagName:"div",className:"history-column flex-column flex-row-container",id:function(){return this.model?"history-column-"+this.model.get("id"):""},initialize:function(a){a=a||{},this.purgeAllowed=_.isUndefined(a.purgeAllowed)?!1:a.purgeAllowed,this.panel=a.panel||this.createPanel(a),this.setUpListeners()},createPanel:function(a){a=_.extend({model:this.model,purgeAllowed:this.purgeAllowed,dragItems:!0},a);var c=new b.HistoryPanelEdit(a);return c._renderEmptyMessage=this.__patch_renderEmptyMessage,c},__patch_renderEmptyMessage:function(a){var b=this,c=_.chain(this.model.get("state_ids")).values().flatten().value().length,d=b.$emptyMessage(a);return _.isEmpty(b.hdaViews)?c&&!this.model.contents.length?d.empty().append($(' loading datasets...')).show():b.searchFor?d.text(b.noneFoundMsg).show():d.text(b.emptyMsg).show():d.hide(),d},setUpListeners:function(){var a=this;this.once("rendered",function(){a.trigger("rendered:initial",a)}),this.setUpPanelListeners()},setUpPanelListeners:function(){var a=this;this.listenTo(this.panel,{rendered:function(){a.trigger("rendered",a)}},this)},inView:function(a,b){var c=this.$el.offset().left,d=c+this.$el.width();return a>d?!1:c>b?!1:!0},$panel:function(){return this.$(".history-panel")},render:function(a){a=void 0!==a?a:"fast";var b=this.model?this.model.toJSON():{};return this.$el.html(this.template(b)),this.renderPanel(a),this.setUpBehaviors(),this},setUpBehaviors:function(){},template:function(a){return a=_.extend(a||{},{isCurrentHistory:this.currentHistory}),$(['
',this.controlsLeftTemplate({history:a,view:this}),this.controlsRightTemplate({history:a,view:this}),"
",'
','
',"
"].join(""))},renderPanel:function(a){return a=void 0!==a?a:"fast",this.panel.setElement(this.$panel()).render(a),this.currentHistory&&this.panel.$list().before(this.panel._renderDropTargetHelp()),this},events:{"click .switch-to.btn":function(){this.model.setAsCurrent()},"click .delete-history":function(){var a=this;this.model._delete().fail(function(a,b,c){alert(_l("Could not delete the history")+":\n"+c)}).done(function(){a.render()})},"click .undelete-history":function(){var a=this;this.model.undelete().fail(function(a,b,c){alert(_l("Could not undelete the history")+":\n"+c)}).done(function(){a.render()})},"click .purge-history":function(){if(confirm(_l("This will permanently remove the data. Are you sure?"))){var a=this;this.model.purge().fail(function(a,b,c){alert(_l("Could not purge the history")+":\n"+c)}).done(function(){a.render()})}},"click .copy-history":"copy"},copy:function(){e(this.model)},controlsLeftTemplate:_.template(['
',"<% if( data.history.isCurrentHistory ){ %>",'',_l("Current History"),"","<% } else { %>",'","<% } %>","
"].join(""),{variable:"data"}),controlsRightTemplate:_.template(['
',"<% if( !data.history.purged ){ %>",'
','",'","
","<% } %>","
"].join(""),{variable:"data"}),toString:function(){return"HistoryPanelColumn("+(this.panel?this.panel:"")+")"}}),g=Backbone.View.extend(c.LoggableMixin).extend({className:"multi-panel-history",initialize:function(a){if(a=a||{},this.log(this+".init",a),this.$el.addClass(this.className),!a.currentHistoryId)throw new Error(this+" requires a currentHistoryId in the options");this.currentHistoryId=a.currentHistoryId,this.options={columnWidth:312,borderWidth:1,columnGap:8,headerHeight:29,footerHeight:0,controlsHeight:20},this.order=a.order||"update",this._initSortOrders(),this.hdaQueue=new d.NamedAjaxQueue([],!1),this.collection=null,this.setCollection(a.histories||[]),this.columnMap={},this.createColumns(a.columnOptions),this.setUpListeners()},_initSortOrders:function(){function a(a,b){return function(c,d,e){return c.id===e?-1:d.id===e?1:(c=a(c),d=a(d),b.asc?c===d?0:c>d?1:-1:c===d?0:c>d?-1:1)}}return this.sortOrders={update:{text:_l("most recent first"),fn:a(function(a){return new Date(a.get("update_time"))},{asc:!1})},name:{text:_l("name, a to z"),fn:a(function(a){return a.get("name")},{asc:!0})},"name-dsc":{text:_l("name, z to a"),fn:a(function(a){return a.get("name")},{asc:!1})},size:{text:_l("size, large to small"),fn:a(function(a){return a.get("size")},{asc:!1})},"size-dsc":{text:_l("size, small to large"),fn:a(function(a){return a.get("size")},{asc:!0})}},this.sortOrders},setUpListeners:function(){},setCollection:function(a){var b=this;return b.stopListening(b.collection),b.collection=a,b.sortCollection(b.order,{silent:!0}),b.setUpCollectionListeners(),b.trigger("new-collection",b),b},setUpCollectionListeners:function(){var a=this,b=a.collection;a.listenTo(b,{add:a.addAsCurrentColumn,"set-as-current":a.setCurrentHistory,"change:deleted change:purged":a.handleDeletedHistory,sort:function(){a.renderColumns(0)}})},setCurrentHistory:function(a){var b=this.columnMap[this.currentHistoryId];b&&(b.currentHistory=!1,b.$el.height("")),this.currentHistoryId=a.id;var c=this.columnMap[this.currentHistoryId];return c.currentHistory=!0,this.sortCollection(),multipanel._recalcFirstColumnHeight(),c},handleDeletedHistory:function(a){if(a.get("deleted")||a.get("purged")){this.log("handleDeletedHistory",this.collection.includeDeleted,a);var b=this;if(column=b.columnMap[a.id],!column)return;column.model.id===this.currentHistoryId||b.collection.includeDeleted||b.removeColumn(column)}},sortCollection:function(a,b){a=_.isUndefined(a)?this.order:a,a in this.sortOrders||(a="update"),this.order=a;var c=this.currentHistoryId,d=this.sortOrders[a];return this.collection.comparator=function(a,b){return d.fn(a,b,c)},this.$(".current-order").text(d.text),this.$(".more-options").is(":visible")&&this.$(".open-more-options.btn").popover("show"),this.collection.sort(b),this.collection},create:function(){return this.collection.create({current:!0})},createColumns:function(a){a=a||{};var b=this;this.columnMap={},b.collection.each(function(c){var d=b.createColumn(c,a);b.columnMap[c.id]=d})},createColumn:function(a,b){b=_.extend({},b,{model:a,purgeAllowed:Galaxy.config.allow_user_dataset_purge});var c=new f(b);return a.id===this.currentHistoryId&&(c.currentHistory=!0),this.setUpColumnListeners(c),c},sortedFilteredColumns:function(a){if(a=a||this.filters,!a||!a.length)return this.sortedColumns();var b=this;return b.sortedColumns().filter(function(b){var c=b.currentHistory||_.every(a.map(function(a){return a.call(b)}));return c})},sortedColumns:function(){var a=this,b=this.collection.map(function(b){return a.columnMap[b.id]});return b},addColumn:function(a,b){b=void 0!==b?b:!0;var c=this.createColumn(a);return this.columnMap[a.id]=c,b&&this.renderColumns(),c},addAsCurrentColumn:function(a){var b=this,c=this.addColumn(a,!1);return this.setCurrentHistory(a),c.once("rendered",function(){b.queueHdaFetch(c)}),c},removeColumn:function(a,b){if(b=void 0!==b?b:!0,this.log("removeColumn",a),a){var c=this,d=this.options.columnWidth+this.options.columnGap;a.$el.fadeOut("fast",function(){b&&($(this).remove(),c.$(".middle").width(c.$(".middle").width()-d),c.checkColumnsInView(),c._recalcFirstColumnHeight()),c.stopListening(a.panel),c.stopListening(a),delete c.columnMap[a.model.id],a.remove()})}},setUpColumnListeners:function(a){var b=this;b.listenTo(a,{"in-view":b.queueHdaFetch}),b.listenTo(a.panel,{"view:draggable:dragstart":function(a){b._dropData=JSON.parse(a.dataTransfer.getData("text")),b.currentColumnDropTargetOn()},"view:draggable:dragend":function(){b._dropData=null,b.currentColumnDropTargetOff()},"droptarget:drop":function(a,c,e){var f=b._dropData.filter(function(a){return e.model.contents.isCopyable(a)});b._dropData=null;var g=new d.NamedAjaxQueue;f.forEach(function(a){g.add({name:"copy-"+a.id,fn:function(){return e.model.contents.copy(a)}})}),g.start(),g.done(function(){e.model.fetch()})}})},columnMapLength:function(){return Object.keys(this.columnMap).length},render:function(a){a=void 0!==a?a:this.fxSpeed;var b=this;return b.log(b+".render"),b.$el.html(b.mainTemplate(b)),b.renderColumns(a),b.setUpBehaviors(),b.trigger("rendered",b),b},renderColumns:function(a){a=void 0!==a?a:this.fxSpeed;var b=this,c=b.sortedFilteredColumns();b.$(".middle").width(c.length*(this.options.columnWidth+this.options.columnGap)+this.options.columnGap+16);var d=b.$(".middle");return d.empty(),c.forEach(function(c){c.$el.appendTo(d),c.delegateEvents(),b.renderColumn(c,a)}),this.searchFor&&c.length<=1||(b.checkColumnsInView(),this._recalcFirstColumnHeight()),c},renderColumn:function(a,b){return b=void 0!==b?b:this.fxSpeed,a.render(b)},queueHdaFetch:function(a){if(0===a.model.contents.length&&!a.model.get("empty")){var b={},c=_.values(a.panel.storage.get("expandedIds")).join();c&&(b.dataset_details=c),this.hdaQueue.add({name:a.model.id,fn:function(){var c=a.model.contents.fetch({data:b,silent:!0});return c.done(function(){a.panel.renderItems()})}}),this.hdaQueue.running||this.hdaQueue.start()}},queueHdaFetchDetails:function(a){(0===a.model.contents.length&&!a.model.get("empty")||!a.model.contents.haveDetails())&&(this.hdaQueue.add({name:a.model.id,fn:function(){var b=a.model.contents.fetch({data:{details:"all"},silent:!0});return b.done(function(){a.panel.renderItems()})}}),this.hdaQueue.running||this.hdaQueue.start())},renderInfo:function(a){return this.$(".header .header-info").text(a)},events:{"click .done.btn":"close","click .create-new.btn":"create","click #include-deleted":"_clickToggleDeletedHistories","click .order .set-order":"_chooseOrder","click #toggle-deleted":"_clickToggleDeletedDatasets","click #toggle-hidden":"_clickToggleHiddenDatasets"},close:function(){var a="/";Galaxy&&Galaxy.options&&Galaxy.options.root?a=Galaxy.options.root:galaxy_config&&galaxy_config.root&&(a=galaxy_config.root),window.location=a},_clickToggleDeletedHistories:function(a){return this.toggleDeletedHistories($(a.currentTarget).is(":checked"))},toggleDeletedHistories:function(a){window.location=a?Galaxy.options.root+"history/view_multiple?include_deleted_histories=True":Galaxy.options.root+"history/view_multiple"},_clickToggleDeletedDatasets:function(a){return this.toggleDeletedDatasets($(a.currentTarget).is(":checked"))},toggleDeletedDatasets:function(a){a=void 0!==a?a:!1;var b=this;b.sortedFilteredColumns().forEach(function(b,c){_.delay(function(){b.panel.toggleShowDeleted(a,!1)},200*c)})},_clickToggleHiddenDatasets:function(a){return this.toggleHiddenDatasets($(a.currentTarget).is(":checked"))},toggleHiddenDatasets:function(a){a=void 0!==a?a:!1;var b=this;b.sortedFilteredColumns().forEach(function(b,c){_.delay(function(){b.panel.toggleShowHidden(a,!1)},200*c)})},_chooseOrder:function(a){var b=$(a.currentTarget).data("order");this.sortCollection(b)},setUpBehaviors:function(){var a=this;a._moreOptionsPopover(),a.$("#search-histories").searchInput({name:"search-histories",placeholder:_l("search histories"),onsearch:function(b){a.searchFor=b,a.filters=[function(){return this.model.matchesAll(a.searchFor)}],a.renderColumns(0)},onclear:function(){a.searchFor=null,a.filters=[],a.renderColumns(0)}}),a.$("#search-datasets").searchInput({name:"search-datasets",placeholder:_l("search all datasets"),onfirstsearch:function(b){a.hdaQueue.clear(),a.$("#search-datasets").searchInput("toggle-loading"),a.searchFor=b,a.sortedFilteredColumns().forEach(function(c){c.panel.searchItems(b),a.queueHdaFetchDetails(c)}),a.hdaQueue.progress(function(b){a.renderInfo([_l("searching"),b.curr+1,_l("of"),b.total].join(" "))}),a.hdaQueue.deferred.done(function(){a.renderInfo(""),a.$("#search-datasets").searchInput("toggle-loading")})},onsearch:function(b){a.searchFor=b,a.sortedFilteredColumns().forEach(function(a){a.panel.searchItems(b)})},onclear:function(){a.searchFor=null,a.sortedFilteredColumns().forEach(function(a){a.panel.clearSearch()})}}),$(window).resize(function(){a._recalcFirstColumnHeight()});var b=_.debounce(_.bind(this.checkColumnsInView,this),100);this.$(".middle").parent().scroll(b)},_moreOptionsPopover:function(){return this.$(".open-more-options.btn").popover({container:".header",placement:"bottom",html:!0,content:$(this.optionsPopoverTemplate(this))})},_recalcFirstColumnHeight:function(){var a=this.$(".history-column").first(),b=this.$(".middle").height(),c=a.find(".panel-controls").height();a.height(b).find(".inner").height(b-c)},_viewport:function(){var a=this.$(".middle").parent().offset().left;return{left:a,right:a+this.$(".middle").parent().width()}},columnsInView:function(){var a=this._viewport();return this.sortedFilteredColumns().filter(function(b){return b.currentHistory||b.inView(a.left,a.right)})},checkColumnsInView:function(){this.columnsInView().forEach(function(a){a.trigger("in-view",a)})},currentColumnDropTargetOn:function(){var a=this.columnMap[this.currentHistoryId];a&&(a.panel.dataDropped=function(){},a.panel.dropTargetOn())},currentColumnDropTargetOff:function(){var a=this.columnMap[this.currentHistoryId];a&&(a.panel.dataDropped=b.HistoryPanelEdit.prototype.dataDrop,a.panel.dropTarget=!1,a.panel.$(".history-drop-target").remove())},toString:function(){return"MultiPanelColumns("+(this.columns?this.columns.length:0)+")"},mainTemplate:_.template(['
','
','",'
','
','','',"","
",'
','
',"
","
",'
',' ","
","
",'
','
',"
",'"].join(""),{variable:"view"}),optionsPopoverTemplate:_.template(['
','
','",'","
",'
","
",'
",'
","
"].join(""),{variable:"view"})});return{MultiPanelColumns:g}}); +define(["mvc/history/history-model","mvc/history/history-panel-edit","mvc/base-mvc","utils/ajax-queue","ui/mode-button","ui/search-input"],function(a,b,c,d){function e(a,b){function c(a){if(!a){if(!Galaxy.modal.$("#invalid-title").size()){var b=$("

").attr("id","invalid-title").css({color:"red","margin-top":"8px"}).addClass("bg-danger").text(_l("Please enter a valid history title"));Galaxy.modal.$(".modal-body").append(b)}return!1}return a}function d(b){var c=$('

Copying history...

').css("margin-top","8px");Galaxy.modal.$(".modal-body").append(c),a.copy(!0,b).fail(function(){alert(_l("History could not be copied. Please contact a Galaxy administrator"))}).always(function(){Galaxy.modal.hide()})}function e(){var a=Galaxy.modal.$("#copy-modal-title").val();c(a)&&d(a)}if(b=b||{},!Galaxy||!Galaxy.modal)return a.copy();var f=a.get("name"),g="Copy of '"+f+"'";Galaxy.modal.show(_.extend({title:_l("Copying history")+' "'+f+'"',body:$(['
",''].join("")),buttons:{Cancel:function(){Galaxy.modal.hide()},Copy:e}},b)),$("#copy-modal-title").focus().select(),$("#copy-modal-title").on("keydown",function(a){13===a.keyCode&&e()})}var f=Backbone.View.extend(c.LoggableMixin).extend({tagName:"div",className:"history-column flex-column flex-row-container",id:function(){return this.model?"history-column-"+this.model.get("id"):""},initialize:function(a){a=a||{},this.purgeAllowed=_.isUndefined(a.purgeAllowed)?!1:a.purgeAllowed,this.panel=a.panel||this.createPanel(a),this.setUpListeners()},createPanel:function(a){a=_.extend({model:this.model,purgeAllowed:this.purgeAllowed,dragItems:!0},a);var c=new b.HistoryPanelEdit(a);return c._renderEmptyMessage=this.__patch_renderEmptyMessage,c},__patch_renderEmptyMessage:function(a){var b=this,c=_.chain(this.model.get("state_ids")).values().flatten().value().length,d=b.$emptyMessage(a);return _.isEmpty(b.hdaViews)?c&&!this.model.contents.length?d.empty().append($(' loading datasets...')).show():b.searchFor?d.text(b.noneFoundMsg).show():d.text(b.emptyMsg).show():d.hide(),d},setUpListeners:function(){var a=this;this.once("rendered",function(){a.trigger("rendered:initial",a)}),this.setUpPanelListeners()},setUpPanelListeners:function(){var a=this;this.listenTo(this.panel,{rendered:function(){a.trigger("rendered",a)}},this)},inView:function(a,b){var c=this.$el.offset().left,d=c+this.$el.width();return a>d?!1:c>b?!1:!0},$panel:function(){return this.$(".history-panel")},render:function(a){a=void 0!==a?a:"fast";var b=this.model?this.model.toJSON():{};return this.$el.html(this.template(b)),this.renderPanel(a),this.setUpBehaviors(),this},setUpBehaviors:function(){},template:function(a){return a=_.extend(a||{},{isCurrentHistory:this.currentHistory}),$(['
',this.controlsLeftTemplate({history:a,view:this}),this.controlsRightTemplate({history:a,view:this}),"
",'
','
',"
"].join(""))},renderPanel:function(a){return a=void 0!==a?a:"fast",this.panel.setElement(this.$panel()).render(a),this.currentHistory&&this.panel.$list().before(this.panel._renderDropTargetHelp()),this},events:{"click .switch-to.btn":function(){this.model.setAsCurrent()},"click .delete-history":function(){var a=this;this.model._delete().fail(function(a,b,c){alert(_l("Could not delete the history")+":\n"+c)}).done(function(){a.render()})},"click .undelete-history":function(){var a=this;this.model.undelete().fail(function(a,b,c){alert(_l("Could not undelete the history")+":\n"+c)}).done(function(){a.render()})},"click .purge-history":function(){if(confirm(_l("This will permanently remove the data. Are you sure?"))){var a=this;this.model.purge().fail(function(a,b,c){alert(_l("Could not purge the history")+":\n"+c)}).done(function(){a.render()})}},"click .copy-history":"copy"},copy:function(){e(this.model)},controlsLeftTemplate:_.template(['
',"<% if( data.history.isCurrentHistory ){ %>",'',_l("Current History"),"","<% } else { %>",'","<% } %>","
"].join(""),{variable:"data"}),controlsRightTemplate:_.template(['
',"<% if( !data.history.purged ){ %>",'
','",'","
","<% } %>","
"].join(""),{variable:"data"}),toString:function(){return"HistoryPanelColumn("+(this.panel?this.panel:"")+")"}}),g=Backbone.View.extend(c.LoggableMixin).extend({className:"multi-panel-history",initialize:function(a){if(a=a||{},this.log(this+".init",a),this.$el.addClass(this.className),!a.currentHistoryId)throw new Error(this+" requires a currentHistoryId in the options");this.currentHistoryId=a.currentHistoryId,this.options={columnWidth:312,borderWidth:1,columnGap:8,headerHeight:29,footerHeight:0,controlsHeight:20},this.order=a.order||"update",this._initSortOrders(),this.hdaQueue=new d.NamedAjaxQueue([],!1),this.collection=null,this.setCollection(a.histories||[]),this.columnMap={},this.createColumns(a.columnOptions),this.setUpListeners()},_initSortOrders:function(){function a(a,b){return function(c,d,e){return c.id===e?-1:d.id===e?1:(c=a(c),d=a(d),b.asc?c===d?0:c>d?1:-1:c===d?0:c>d?-1:1)}}return this.sortOrders={update:{text:_l("most recent first"),fn:a(function(a){return new Date(a.get("update_time"))},{asc:!1})},name:{text:_l("name, a to z"),fn:a(function(a){return a.get("name")},{asc:!0})},"name-dsc":{text:_l("name, z to a"),fn:a(function(a){return a.get("name")},{asc:!1})},size:{text:_l("size, large to small"),fn:a(function(a){return a.get("size")},{asc:!1})},"size-dsc":{text:_l("size, small to large"),fn:a(function(a){return a.get("size")},{asc:!0})}},this.sortOrders},setUpListeners:function(){},setCollection:function(a){var b=this;return b.stopListening(b.collection),b.collection=a,b.sortCollection(b.order,{silent:!0}),b.setUpCollectionListeners(),b.trigger("new-collection",b),b},setUpCollectionListeners:function(){var a=this,b=a.collection;a.listenTo(b,{add:a.addAsCurrentColumn,"set-as-current":a.setCurrentHistory,"change:deleted change:purged":a.handleDeletedHistory,sort:function(){a.renderColumns(0)}})},setCurrentHistory:function(a){var b=this.columnMap[this.currentHistoryId];b&&(b.currentHistory=!1,b.$el.height("")),this.currentHistoryId=a.id;var c=this.columnMap[this.currentHistoryId];return c.currentHistory=!0,this.sortCollection(),multipanel._recalcFirstColumnHeight(),c},handleDeletedHistory:function(a){if(a.get("deleted")||a.get("purged")){this.log("handleDeletedHistory",this.collection.includeDeleted,a);var b=this;if(column=b.columnMap[a.id],!column)return;column.model.id===this.currentHistoryId||b.collection.includeDeleted||b.removeColumn(column)}},sortCollection:function(a,b){a=_.isUndefined(a)?this.order:a,a in this.sortOrders||(a="update"),this.order=a;var c=this.currentHistoryId,d=this.sortOrders[a];return this.collection.comparator=function(a,b){return d.fn(a,b,c)},this.$(".current-order").text(d.text),this.$(".more-options").is(":visible")&&this.$(".open-more-options.btn").popover("show"),this.collection.sort(b),this.collection},create:function(){return this.collection.create({current:!0})},createColumns:function(a){a=a||{};var b=this;this.columnMap={},b.collection.each(function(c){var d=b.createColumn(c,a);b.columnMap[c.id]=d})},createColumn:function(a,b){b=_.extend({},b,{model:a,purgeAllowed:Galaxy.config.allow_user_dataset_purge});var c=new f(b);return a.id===this.currentHistoryId&&(c.currentHistory=!0),this.setUpColumnListeners(c),c},sortedFilteredColumns:function(a){if(a=a||this.filters,!a||!a.length)return this.sortedColumns();var b=this;return b.sortedColumns().filter(function(b){var c=b.currentHistory||_.every(a.map(function(a){return a.call(b)}));return c})},sortedColumns:function(){var a=this,b=this.collection.map(function(b){return a.columnMap[b.id]});return b},addColumn:function(a,b){b=void 0!==b?b:!0;var c=this.createColumn(a);return this.columnMap[a.id]=c,b&&this.renderColumns(),c},addAsCurrentColumn:function(a){var b=this,c=this.addColumn(a,!1);return this.setCurrentHistory(a),c.once("rendered",function(){b.queueHdaFetch(c)}),c},removeColumn:function(a,b){if(b=void 0!==b?b:!0,this.log("removeColumn",a),a){var c=this,d=this.options.columnWidth+this.options.columnGap;a.$el.fadeOut("fast",function(){b&&($(this).remove(),c.$(".middle").width(c.$(".middle").width()-d),c.checkColumnsInView(),c._recalcFirstColumnHeight()),c.stopListening(a.panel),c.stopListening(a),delete c.columnMap[a.model.id],a.remove()})}},setUpColumnListeners:function(a){var b=this;b.listenTo(a,{"in-view":b.queueHdaFetch}),b.listenTo(a.panel,{"view:draggable:dragstart":function(a){b._dropData=JSON.parse(a.dataTransfer.getData("text")),b.currentColumnDropTargetOn()},"view:draggable:dragend":function(){b._dropData=null,b.currentColumnDropTargetOff()},"droptarget:drop":function(a,c,e){var f=b._dropData.filter(function(a){return e.model.contents.isCopyable(a)});b._dropData=null;var g=new d.NamedAjaxQueue;f.forEach(function(a){g.add({name:"copy-"+a.id,fn:function(){return e.model.contents.copy(a)}})}),g.start(),g.done(function(){e.model.fetch()})}})},columnMapLength:function(){return Object.keys(this.columnMap).length},render:function(a){a=void 0!==a?a:this.fxSpeed;var b=this;return b.log(b+".render"),b.$el.html(b.mainTemplate(b)),b.renderColumns(a),b.setUpBehaviors(),b.trigger("rendered",b),b},renderColumns:function(a){a=void 0!==a?a:this.fxSpeed;var b=this,c=b.sortedFilteredColumns();b.$(".middle").width(c.length*(this.options.columnWidth+this.options.columnGap)+this.options.columnGap+16);var d=b.$(".middle");return d.empty(),c.forEach(function(c){c.$el.appendTo(d),c.delegateEvents(),b.renderColumn(c,a)}),this.searchFor&&c.length<=1||(b.checkColumnsInView(),this._recalcFirstColumnHeight()),c},renderColumn:function(a,b){return b=void 0!==b?b:this.fxSpeed,a.render(b)},queueHdaFetch:function(a){if(0===a.model.contents.length&&!a.model.get("empty")){var b={},c=_.values(a.panel.storage.get("expandedIds")).join();c&&(b.dataset_details=c),this.hdaQueue.add({name:a.model.id,fn:function(){var c=a.model.contents.fetch({data:b,silent:!0});return c.done(function(){a.panel.renderItems()})}}),this.hdaQueue.running||this.hdaQueue.start()}},queueHdaFetchDetails:function(a){(0===a.model.contents.length&&!a.model.get("empty")||!a.model.contents.haveDetails())&&(this.hdaQueue.add({name:a.model.id,fn:function(){var b=a.model.contents.fetch({data:{details:"all"},silent:!0});return b.done(function(){a.panel.renderItems()})}}),this.hdaQueue.running||this.hdaQueue.start())},renderInfo:function(a){return this.$(".header .header-info").text(a)},events:{"click .done.btn":"close","click .create-new.btn":"create","click #include-deleted":"_clickToggleDeletedHistories","click .order .set-order":"_chooseOrder","click #toggle-deleted":"_clickToggleDeletedDatasets","click #toggle-hidden":"_clickToggleHiddenDatasets"},close:function(){var a="/";Galaxy&&Galaxy.options&&Galaxy.options.root?a=Galaxy.options.root:galaxy_config&&galaxy_config.root&&(a=galaxy_config.root),window.location=a},_clickToggleDeletedHistories:function(a){return this.toggleDeletedHistories($(a.currentTarget).is(":checked"))},toggleDeletedHistories:function(a){window.location=a?Galaxy.options.root+"history/view_multiple?include_deleted_histories=True":Galaxy.options.root+"history/view_multiple"},_clickToggleDeletedDatasets:function(a){return this.toggleDeletedDatasets($(a.currentTarget).is(":checked"))},toggleDeletedDatasets:function(a){a=void 0!==a?a:!1;var b=this;b.sortedFilteredColumns().forEach(function(b,c){_.delay(function(){b.panel.toggleShowDeleted(a,!1)},200*c)})},_clickToggleHiddenDatasets:function(a){return this.toggleHiddenDatasets($(a.currentTarget).is(":checked"))},toggleHiddenDatasets:function(a){a=void 0!==a?a:!1;var b=this;b.sortedFilteredColumns().forEach(function(b,c){_.delay(function(){b.panel.toggleShowHidden(a,!1)},200*c)})},_chooseOrder:function(a){var b=$(a.currentTarget).data("order");this.sortCollection(b)},setUpBehaviors:function(){var a=this;a._moreOptionsPopover(),a.$("#search-histories").searchInput({name:"search-histories",placeholder:_l("search histories"),onsearch:function(b){a.searchFor=b,a.filters=[function(){return this.model.matchesAll(a.searchFor)}],a.renderColumns(0)},onclear:function(){a.searchFor=null,a.filters=[],a.renderColumns(0)}}),a.$("#search-datasets").searchInput({name:"search-datasets",placeholder:_l("search all datasets"),onfirstsearch:function(b){a.hdaQueue.clear(),a.$("#search-datasets").searchInput("toggle-loading"),a.searchFor=b,a.sortedFilteredColumns().forEach(function(c){c.panel.searchItems(b),a.queueHdaFetchDetails(c)}),a.hdaQueue.progress(function(b){a.renderInfo([_l("searching"),b.curr+1,_l("of"),b.total].join(" "))}),a.hdaQueue.deferred.done(function(){a.renderInfo(""),a.$("#search-datasets").searchInput("toggle-loading")})},onsearch:function(b){a.searchFor=b,a.sortedFilteredColumns().forEach(function(a){a.panel.searchItems(b)})},onclear:function(){a.searchFor=null,a.sortedFilteredColumns().forEach(function(a){a.panel.clearSearch()})}}),$(window).resize(function(){a._recalcFirstColumnHeight()});var b=_.debounce(function(){a.checkColumnsInView()},100);this.$(".middle").parent().scroll(b)},_moreOptionsPopover:function(){return this.$(".open-more-options.btn").popover({container:".header",placement:"bottom",html:!0,content:$(this.optionsPopoverTemplate(this))})},_recalcFirstColumnHeight:function(){var a=this.$(".history-column").first(),b=this.$(".middle").height(),c=a.find(".panel-controls").height();a.height(b).find(".inner").height(b-c)},_viewport:function(){var a=this.$(".middle").parent().offset().left;return{left:a,right:a+this.$(".middle").parent().width()}},columnsInView:function(){var a=this._viewport();return this.sortedFilteredColumns().filter(function(b){return b.currentHistory||b.inView(a.left,a.right)})},checkColumnsInView:function(){this.columnsInView().forEach(function(a){a.trigger("in-view",a)})},currentColumnDropTargetOn:function(){var a=this.columnMap[this.currentHistoryId];a&&(a.panel.dataDropped=function(){},a.panel.dropTargetOn())},currentColumnDropTargetOff:function(){var a=this.columnMap[this.currentHistoryId];a&&(a.panel.dataDropped=b.HistoryPanelEdit.prototype.dataDrop,a.panel.dropTarget=!1,a.panel.$(".history-drop-target").remove())},toString:function(){return"MultiPanelColumns("+(this.columns?this.columns.length:0)+")"},mainTemplate:_.template(['
','
','",'
','
','','',"","
",'
','
',"
","
",'
',' ","
","
",'
','
',"
",'"].join(""),{variable:"view"}),optionsPopoverTemplate:_.template(['
','
','",'","
",'
","
",'
",'
","
"].join(""),{variable:"view"})});return{MultiPanelColumns:g}}); //# sourceMappingURL=../../../maps/mvc/history/multi-panel.js.map \ No newline at end of file diff --git a/static/style/blue/base.css b/static/style/blue/base.css index 207da8938da..27c0ed68afc 100644 --- a/static/style/blue/base.css +++ b/static/style/blue/base.css @@ -1442,7 +1442,7 @@ html[dir="rtl"] .select2-container-multi .select2-search-choice-close{left:auto; .ui-portlet .portlet-backdrop,.ui-portlet-limited .portlet-backdrop,.ui-portlet-narrow .portlet-backdrop{display:none;z-index:10;position:absolute;top:0px;width:100%;height:100%;opacity:0.2;background:#fff;cursor:not-allowed} .ui-portlet-repeat,.ui-portlet-section,.ui-portlet-section{border:none;border-left:solid 3px #ebd9b2;border-radius:5px;margin-bottom:5px}.ui-portlet-repeat .portlet-header,.ui-portlet-section .portlet-header{background:#ebd9b2;border-radius:5px;border-bottom-left-radius:0px;border-top-left-radius:0px;padding:0px 2px}.ui-portlet-repeat .portlet-header .portlet-title-text,.ui-portlet-section .portlet-header .portlet-title-text{vertical-align:middle;line-height:20px !important} .ui-portlet-repeat .portlet-content,.ui-portlet-section .portlet-content{padding-right:0px} -.ui-portlet-section{border-left:solid 3px #dfe5f9}.ui-portlet-section .portlet-header{background:#dfe5f9;border-bottom:solid #b4c2f1 1px} +.ui-portlet-section{margin-top:5px;border-left:solid 3px #dfe5f9}.ui-portlet-section .portlet-header{background:#dfe5f9;border-bottom:solid #b4c2f1 1px} .ui-portlet-narrow{border:none}.ui-portlet-narrow .portlet-header{border-radius:3px}.ui-portlet-narrow .portlet-header .portlet-operations .ui-button-icon{margin-left:3px} .ui-portlet-narrow .ui-portlet-repeat .portlet-header,.ui-portlet-narrow .ui-portlet-section .portlet-header{border-radius:5px;border-bottom-left-radius:0px;border-top-left-radius:0px} .ui-portlet-narrow .portlet-content{padding:0px} diff --git a/static/style/src/less/ui.less b/static/style/src/less/ui.less index ef8b2d0acb5..adf9a0fe5b2 100644 --- a/static/style/src/less/ui.less +++ b/static/style/src/less/ui.less @@ -272,6 +272,7 @@ .ui-portlet-section { &:extend(.ui-portlet-repeat all); + margin-top: @ui-margin-vertical; border-left: solid 3px @side-panel-bg; .portlet-header { background: @side-panel-bg; diff --git a/templates/user/info.mako b/templates/user/info.mako index 7447085a722..5ecc1847c7a 100644 --- a/templates/user/info.mako +++ b/templates/user/info.mako @@ -9,7 +9,7 @@ function validateString(test_string, type) { var mail_re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - var username_re = /^[a-z0-9\-]{4,255}$/; + var username_re = /^[a-z0-9\-]{3,255}$/; if (type === 'email') { return mail_re.test(test_string); } else if (type === 'username'){ @@ -47,7 +47,7 @@ $( '#login_info' ).bind( 'submit', function( e ) { var error_text_email= 'Please enter your valid email address.'; var error_text_email_long= 'Email cannot be more than 255 characters in length.'; - var error_text_username_characters = 'Public name must contain only lowercase letters, numbers and "-". It also has to be shorter than 255 characters but longer than 3.'; + var error_text_username_characters = 'Public name must contain only lowercase letters, numbers and "-". It also has to be shorter than 255 characters but longer than 2.'; var email = $( '#email_input' ).val(); var name = $( '#name_input' ).val(); var validForm = true; @@ -108,7 +108,7 @@
Your public name provides a means of identifying you publicly within this tool shed. Public - names must be at least four characters in length and contain only lower-case letters, numbers, + names must be at least three characters in length and contain only lower-case letters, numbers, and the '-' character. You cannot change your public name after you have created a repository in this tool shed.
@@ -117,7 +117,7 @@
Your public name is an optional identifier that will be used to generate addresses for information - you share publicly. Public names must be at least four characters in length and contain only lower-case + you share publicly. Public names must be at least three characters in length and contain only lower-case letters, numbers, and the '-' character.
%endif diff --git a/templates/user/login.mako b/templates/user/login.mako index 26358f5e062..91c58604597 100644 --- a/templates/user/login.mako +++ b/templates/user/login.mako @@ -80,8 +80,8 @@ def inherit(context):
Login
- - + +
diff --git a/templates/user/register.mako b/templates/user/register.mako index 549d8f77d7b..68393e1b703 100644 --- a/templates/user/register.mako +++ b/templates/user/register.mako @@ -96,7 +96,7 @@ def inherit(context): var error_text_email= 'Please enter your valid email address'; var error_text_email_long= 'Email cannot be more than 255 characters in length'; - var error_text_username_characters = 'Public name must contain only lowercase letters, numbers and "-". It also has to be shorter than 255 characters but longer than 3.'; + var error_text_username_characters = 'Public name must contain only lowercase letters, numbers and "-". It also has to be shorter than 255 characters but longer than 2.'; var error_text_password_short = 'Please use a password of at least 6 characters'; var error_text_password_match = "Passwords don't match"; @@ -141,13 +141,13 @@ def inherit(context): %if t.webapp.name == 'galaxy':
Your public name is an identifier that will be used to generate addresses for information - you share publicly. Public names must be at least four characters in length and contain only lower-case + you share publicly. Public names must be at least three characters in length and contain only lower-case letters, numbers, and the '-' character.
%else:
Your public name provides a means of identifying you publicly within this tool shed. Public - names must be at least four characters in length and contain only lower-case letters, numbers, + names must be at least three characters in length and contain only lower-case letters, numbers, and the '-' character. You cannot change your public name after you have created a repository in this tool shed.
diff --git a/templates/webapps/galaxy/galaxy.masthead.mako b/templates/webapps/galaxy/galaxy.masthead.mako index f5061dfa9b4..c89be9adef2 100644 --- a/templates/webapps/galaxy/galaxy.masthead.mako +++ b/templates/webapps/galaxy/galaxy.masthead.mako @@ -12,15 +12,15 @@ 'use_remote_user' : app.config.use_remote_user, 'remote_user_logout_href' : app.config.remote_user_logout_href, 'enable_cloud_launch' : app.config.get_bool('enable_cloud_launch', False), - 'lims_doc_url' : app.config.get("lims_doc_url", "http://main.g2.bx.psu.edu/u/rkchak/p/sts"), + 'lims_doc_url' : app.config.get("lims_doc_url", "https://usegalaxy.org/u/rkchak/p/sts"), 'biostar_url' : app.config.biostar_url, 'biostar_url_redirect' : h.url_for( controller='biostar', action='biostar_redirect', qualified=True ), - 'support_url' : app.config.get("support_url", "http://wiki.galaxyproject.org/Support"), + 'support_url' : app.config.get("support_url", "https://wiki.galaxyproject.org/Support"), 'search_url' : app.config.get("search_url", "http://galaxyproject.org/search/usegalaxy/"), - 'mailing_lists' : app.config.get("mailing_lists", "http://wiki.galaxyproject.org/MailingLists"), - 'screencasts_url' : app.config.get("screencasts_url", "http://vimeo.com/galaxyproject"), - 'wiki_url' : app.config.get("wiki_url", "http://galaxyproject.org/"), - 'citation_url' : app.config.get("citation_url", "http://wiki.galaxyproject.org/CitingGalaxy"), + 'mailing_lists' : app.config.get("mailing_lists", "https://wiki.galaxyproject.org/MailingLists"), + 'screencasts_url' : app.config.get("screencasts_url", "https://vimeo.com/galaxyproject"), + 'wiki_url' : app.config.get("wiki_url", "https://wiki.galaxyproject.org/"), + 'citation_url' : app.config.get("citation_url", "https://wiki.galaxyproject.org/CitingGalaxy"), 'terms_url' : app.config.get("terms_url", ""), 'allow_user_creation' : app.config.allow_user_creation, 'logo_url' : h.url_for(app.config.get( 'logo_url', '/')), diff --git a/templates/webapps/galaxy/tool_form.mako b/templates/webapps/galaxy/tool_form.mako index 9f7d986b62e..d272fc2c085 100644 --- a/templates/webapps/galaxy/tool_form.mako +++ b/templates/webapps/galaxy/tool_form.mako @@ -10,6 +10,7 @@ ${h.css('base', 'jquery-ui/smoothness/jquery-ui')} ## This avoids making two separate requests since the classic form requires the mako anyway. params = dict(trans.request.params) params['__dataset_id__'] = params.get('id', None) + params['__job_id__'] = params.get('job_id', None) self.form_config = tool.to_json(trans, params) self.form_config.update({ 'id' : tool.id, @@ -226,7 +227,7 @@ ${h.css('base', 'jquery-ui/smoothness/jquery-ui')} ##
%endif - %else: + %elif input.type != "section": ${row_for_param( prefix, input, tool_state, errors, other_values )} %endif %endfor diff --git a/templates/webapps/galaxy/workflow/run.mako b/templates/webapps/galaxy/workflow/run.mako index b70f07142dd..dfcdd9ec189 100644 --- a/templates/webapps/galaxy/workflow/run.mako +++ b/templates/webapps/galaxy/workflow/run.mako @@ -154,11 +154,11 @@ $(".workflow-edit-button").on("click",function(){ var state = $(this).attr("name"); - var stepToolBox = $(this).parent().find('input:not([class]):not([type="hidden"]), select:not([class])'); - var labels = $(this).parent().find('label'); + var stepToolBox = $(this).parent().parent().find('.editable-param').find('input:not([class]):not([type="hidden"]), select:not([class])'); + var labels = $(this).parent().parent().find('.editable-param').find('label'); var split_name = stepToolBox.attr("name").split("|"); var step_id = split_name[0]; - var step_name = split_name.slice(1, split_name.length).join("|"); + var step_name = split_name.slice(2, split_name.length).join("|"); var hidden_html = ""; var html = ""; if (state === "edit"){ @@ -179,9 +179,9 @@ } else{ $(this).parent().find(".editable").hide(); + $(this).parent().find(".editable").empty(); $(this).parent().parent().find(".uneditable_field").show(); $(this).attr("name", "edit"); - stepToolBox.hide(); readyParameter($(this)); } }).each(function(i, icon) { @@ -281,6 +281,9 @@ .editable { display: none; } + .editable-param { + display: none; + } .workflow-edit-button-editing { color: black; @@ -448,11 +451,17 @@ if wf_parms: - ${param.get_html_field( t, value, other_values).get_html( str(step.id) + "|"+ prefix )} + + + ${param.get_html_field( t, value, other_values).get_html( str(step.id) + "|"+ "editable" + "|" + prefix )} + %endif %endif diff --git a/templates/webapps/tool_shed/base_panels.mako b/templates/webapps/tool_shed/base_panels.mako index ed17535b6cd..645bf8fdabe 100644 --- a/templates/webapps/tool_shed/base_panels.mako +++ b/templates/webapps/tool_shed/base_panels.mako @@ -90,13 +90,13 @@ if qa_url: menu_options = [ [_('Galaxy Q&A'), qa_url, "_blank" ] ] menu_options.extend( [ - [_('Tool Shed Wiki'), app.config.get( "wiki_url", "http://wiki.galaxyproject.org/Tool%20Shed" ), "_blank" ], - [_('Support'), app.config.get( "support_url", "http://wiki.galaxyproject.org/Support" ), "_blank" ], + [_('Tool Shed Wiki'), app.config.get( "wiki_url", "https://wiki.galaxyproject.org/ToolShed" ), "_blank" ], + [_('Support'), app.config.get( "support_url", "https://wiki.galaxyproject.org/Support" ), "_blank" ], [_('Search'), app.config.get( "search_url", "http://galaxyproject.org/search/usegalaxy/" ), "_blank" ], - [_('Mailing Lists'), app.config.get( "mailing_lists_url", "http://wiki.galaxyproject.org/MailingLists" ), "_blank" ], - [_('Videos'), app.config.get( "screencasts_url", "http://vimeo.com/galaxyproject" ), "_blank" ], + [_('Mailing Lists'), app.config.get( "mailing_lists_url", "https://wiki.galaxyproject.org/MailingLists" ), "_blank" ], + [_('Videos'), app.config.get( "screencasts_url", "https://vimeo.com/galaxyproject" ), "_blank" ], [_('Wiki'), app.config.get( "wiki_url", "http://galaxyproject.org/" ), "_blank" ], - [_('How to Cite Galaxy'), app.config.get( "citation_url", "http://wiki.galaxyproject.org/CitingGalaxy" ), "_blank" ] + [_('How to Cite Galaxy'), app.config.get( "citation_url", "https://wiki.galaxyproject.org/CitingGalaxy" ), "_blank" ] ] ) tab( "help", _("Help"), None, menu_options=menu_options ) %> diff --git a/templates/webapps/tool_shed/repository/tool_form.mako b/templates/webapps/tool_shed/repository/tool_form.mako index 7025cf4e920..c3f86cd4d4d 100644 --- a/templates/webapps/tool_shed/repository/tool_form.mako +++ b/templates/webapps/tool_shed/repository/tool_form.mako @@ -21,7 +21,7 @@ %for input_index, input in enumerate( inputs.itervalues() ): %if not input.visible: <% pass %> - %elif input.type == "repeat": + %elif input.type in ["repeat", "section"]:
${input.title_plural} diff --git a/test/api/test_tools.py b/test/api/test_tools.py index a0510b751fe..88a9cd6f8ce 100644 --- a/test/api/test_tools.py +++ b/test/api/test_tools.py @@ -58,7 +58,7 @@ class ToolsTestCase( api.ApiTestCase ): self._assert_has_keys( case2, "value", "inputs" ) case2_inputs = case2[ "inputs" ] assert len( case2_inputs ) == 1 - self._assert_has_keys( case2_inputs[ 0 ], 'name', 'type', 'label', 'help' ) + self._assert_has_keys( case2_inputs[ 0 ], 'name', 'type', 'label', 'help', 'argument' ) assert case2_inputs[ 0 ][ "name" ] == "seed" def _show_valid_tool( self, tool_id ): diff --git a/test/api/yaml_to_workflow.py b/test/api/yaml_to_workflow.py index 6771fa252b9..890180ccff5 100644 --- a/test/api/yaml_to_workflow.py +++ b/test/api/yaml_to_workflow.py @@ -17,7 +17,7 @@ STEP_TYPE_ALIASES = { def yaml_to_workflow(has_yaml): as_python = yaml.load(has_yaml) - return python_to_workflow(as_path) + return python_to_workflow(as_python) def python_to_workflow(as_python): diff --git a/test/casperjs/spaceghost.js b/test/casperjs/spaceghost.js index d8f8bcf7e9f..a1dfa1cadc9 100644 --- a/test/casperjs/spaceghost.js +++ b/test/casperjs/spaceghost.js @@ -532,7 +532,7 @@ SpaceGhost.prototype.openHomePage = function openHomePage( then, delay ){ }, then, function openHomePageTimeout(){ - throw new GalaxyError( 'Homepage timed out' ); + throw new GalaxyError( 'Homepage timed out - are you sure your instance is running?' ); }, delay ); diff --git a/test/functional/tools/README.txt b/test/functional/tools/README.txt index 39bb6e748be..f4d08778269 100644 --- a/test/functional/tools/README.txt +++ b/test/functional/tools/README.txt @@ -5,11 +5,11 @@ these tests. Pass in an '-id' along with one of these tool ids to test a single tool. Some API tests use these tools to test various features of the API, -tool, and workflow subsystems. Pass the arugment +tool, and workflow subsystems. Pass the argument '-with_framework_test_tools' to 'run_tests.sh' in addition to '-api' to ensure these tools get loaded during the testing process. -Finally, to play around witht these tools interactively - simply +Finally, to play around with these tools interactively - simply replace the 'galaxy.ini' option 'tool_config_file' with: tool_config_file = test/functional/tools/samples_tool_conf.xml diff --git a/test/functional/tools/inheritance_simple.xml b/test/functional/tools/inheritance_simple.xml new file mode 100644 index 00000000000..218e9653112 --- /dev/null +++ b/test/functional/tools/inheritance_simple.xml @@ -0,0 +1,24 @@ + + (demonstrates subtypes are usable for parent format) + + cat $input1 > $out_file1 + + + + + + + + + + + + + + + + + + + + diff --git a/test/functional/tools/metadata_bam.xml b/test/functional/tools/metadata_bam.xml new file mode 100644 index 00000000000..aa8a52b43d8 --- /dev/null +++ b/test/functional/tools/metadata_bam.xml @@ -0,0 +1,25 @@ + + echo "${ref_names}" > "${output_of_input_metadata}" + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/functional/tools/samples_tool_conf.xml b/test/functional/tools/samples_tool_conf.xml index 8e7c621339c..2c3961dd21e 100644 --- a/test/functional/tools/samples_tool_conf.xml +++ b/test/functional/tools/samples_tool_conf.xml @@ -2,6 +2,7 @@ + @@ -14,6 +15,7 @@ + @@ -28,6 +30,7 @@ + diff --git a/test/functional/tools/section.xml b/test/functional/tools/section.xml new file mode 100644 index 00000000000..29bb7fe10e3 --- /dev/null +++ b/test/functional/tools/section.xml @@ -0,0 +1,43 @@ + + + echo "$int.inttest" >> $out_file1; + echo "$float.floattest" >> $out_file1; + + +
+ +
+
+ +
+
+ + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + + + +
+
+
diff --git a/test/functional/tools/simple_constructs.xml b/test/functional/tools/simple_constructs.xml index b543af03a89..d67c0c282ee 100644 --- a/test/functional/tools/simple_constructs.xml +++ b/test/functional/tools/simple_constructs.xml @@ -1,10 +1,13 @@ - + echo "$p1.p1val" >> $out_file1; echo "$booltest" >> $out_file1; echo "$inttest" >> $out_file1; - echo "$floattest" >> $out_file1; - cat "$files[0].file" >> $out_file1; + echo "$floattest" >> $out_file1; + echo "$radio_select" >> $out_file1; + echo "$check_select" >> $out_file1; + echo "$drop_select" >> $out_file1; + cat "$files[0].file" >> $out_file1; @@ -19,6 +22,21 @@ + + + + + + + + + + + + + + + diff --git a/test/qunit/tests/galaxy-app-base.js b/test/qunit/tests/galaxy-app-base.js index 3f56be384df..30c2de54b70 100644 --- a/test/qunit/tests/galaxy-app-base.js +++ b/test/qunit/tests/galaxy-app-base.js @@ -3,7 +3,7 @@ define( 'bootstrapped-data', function(){ config : { "allow_user_deletion": false, "allow_user_creation": true, - "wiki_url": "http://wiki.galaxyproject.org/", + "wiki_url": "https://wiki.galaxyproject.org/", "ftp_upload_site": null, "support_url": "https://wiki.galaxyproject.org/Support", "allow_user_dataset_purge": false, @@ -98,7 +98,7 @@ define([ ok( app.hasOwnProperty( 'config' ) && typeof app.config === 'object' ); equal( app.config.allow_user_deletion, false ); equal( app.config.allow_user_creation, true ); - equal( app.config.wiki_url, "http://wiki.galaxyproject.org/" ); + equal( app.config.wiki_url, "https://wiki.galaxyproject.org/" ); equal( app.config.ftp_upload_site, null ); //... }); diff --git a/test/unit/managers/mock.py b/test/unit/managers/mock.py index 8d349b84cac..adc67c6a52d 100644 --- a/test/unit/managers/mock.py +++ b/test/unit/managers/mock.py @@ -16,6 +16,8 @@ from galaxy import objectstore from galaxy.model import mapping from galaxy.util.bunch import Bunch +from galaxy.managers import tags +from galaxy import quota # ============================================================================= class OpenObject( object ): @@ -29,6 +31,8 @@ class MockApp( object ): self.model = mapping.init( "/tmp", "sqlite:///:memory:", create_tables=True, object_store=self.object_store ) self.security_agent = self.model.security_agent self.visualizations_registry = MockVisualizationsRegistry() + self.tag_handler = tags.GalaxyTagManager( self ) + self.quota_agent = quota.QuotaAgent( self.model ) class MockAppConfig( Bunch ): def __init__( self, **kwargs ): @@ -53,10 +57,15 @@ class MockWebapp( object ): def __init__( self, **kwargs ): self.name = kwargs.get( 'name', 'galaxy' ) +class MockVisualizationsRegistry( object ): + def get_visualizations( self, trans, target ): + return [] + class MockTrans( object ): def __init__( self, user=None, history=None, **kwargs ): self.app = MockApp( **kwargs ) + self.model = self.app.model self.webapp = MockWebapp( **kwargs ) self.sa_session = self.app.model.session @@ -91,7 +100,3 @@ class MockTrans( object ): template = template_lookup.get_template( filename ) template.output_encoding = 'utf-8' return template.render( **kwargs ) - -class MockVisualizationsRegistry( object ): - def get_visualizations( self, trans, target ): - return [] diff --git a/test/unit/managers/test_CollectionManager.py b/test/unit/managers/test_CollectionManager.py index 427da4b53bd..cbc0eecc53d 100644 --- a/test/unit/managers/test_CollectionManager.py +++ b/test/unit/managers/test_CollectionManager.py @@ -36,10 +36,10 @@ class DatasetCollectionManagerTestCase( BaseTestCase ): def set_up_managers( self ): super( DatasetCollectionManagerTestCase, self ).set_up_managers() - self.dataset_mgr = DatasetManager( self.app ) - self.hda_mgr = HDAManager( self.app ) - self.history_mgr = HistoryManager( self.app ) - self.collection_mgr = DatasetCollectionManager( self.app ) + self.dataset_manager = DatasetManager( self.app ) + self.hda_manager = HDAManager( self.app ) + self.history_manager = HistoryManager( self.app ) + self.collection_manager = DatasetCollectionManager( self.app ) def build_element_identifiers( self, elements ): identifier_list = [] @@ -54,20 +54,20 @@ class DatasetCollectionManagerTestCase( BaseTestCase ): return identifier_list def test_create_simple_list( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) + owner = self.user_manager.create( **user2_data ) - history = self.history_mgr.create( self.trans, name='history1', user=owner ) + history = self.history_manager.create( name='history1', user=owner ) - hda1 = self.hda_mgr.create( self.trans, name='one', - history=history, dataset=self.dataset_mgr.create( self.trans ) ) - hda2 = self.hda_mgr.create( self.trans, name='two', - history=history, dataset=self.dataset_mgr.create( self.trans ) ) - hda3 = self.hda_mgr.create( self.trans, name='three', - history=history, dataset=self.dataset_mgr.create( self.trans ) ) + hda1 = self.hda_manager.create( name='one', + history=history, dataset=self.dataset_manager.create() ) + hda2 = self.hda_manager.create( name='two', + history=history, dataset=self.dataset_manager.create() ) + hda3 = self.hda_manager.create( name='three', + history=history, dataset=self.dataset_manager.create() ) self.log( "should be able to create a new Collection via ids" ) element_identifiers = self.build_element_identifiers( [ hda1, hda2, hda3 ] ) - hdca = self.collection_mgr.create( self.trans, history, 'test collection', 'list', + hdca = self.collection_manager.create( self.trans, history, 'test collection', 'list', element_identifiers=element_identifiers ) self.assertIsInstance( hdca, model.HistoryDatasetCollectionAssociation ) self.assertEqual( hdca.name, 'test collection' ) @@ -117,26 +117,26 @@ class DatasetCollectionManagerTestCase( BaseTestCase ): self.log( "should be able to create a new Collection via objects" ) elements = dict( one=hda1, two=hda2, three=hda3 ) - hdca2 = self.collection_mgr.create( self.trans, history, 'test collection 2', 'list', elements=elements ) + hdca2 = self.collection_manager.create( self.trans, history, 'test collection 2', 'list', elements=elements ) self.assertIsInstance( hdca2, model.HistoryDatasetCollectionAssociation ) def test_update_from_dict( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) + owner = self.user_manager.create( **user2_data ) - history = self.history_mgr.create( self.trans, name='history1', user=owner ) + history = self.history_manager.create( name='history1', user=owner ) - hda1 = self.hda_mgr.create( self.trans, name='one', - history=history, dataset=self.dataset_mgr.create( self.trans ) ) - hda2 = self.hda_mgr.create( self.trans, name='two', - history=history, dataset=self.dataset_mgr.create( self.trans ) ) - hda3 = self.hda_mgr.create( self.trans, name='three', - history=history, dataset=self.dataset_mgr.create( self.trans ) ) + hda1 = self.hda_manager.create( name='one', + history=history, dataset=self.dataset_manager.create() ) + hda2 = self.hda_manager.create( name='two', + history=history, dataset=self.dataset_manager.create() ) + hda3 = self.hda_manager.create( name='three', + history=history, dataset=self.dataset_manager.create() ) elements = dict( one=hda1, two=hda2, three=hda3 ) - hdca = self.collection_mgr.create( self.trans, history, 'test collection', 'list', elements=elements ) + hdca = self.collection_manager.create( self.trans, history, 'test collection', 'list', elements=elements ) self.log( "should be set from a dictionary" ) - self.collection_mgr._set_from_dict( self.trans, hdca, { + self.collection_manager._set_from_dict( self.trans, hdca, { 'deleted' : True, 'visible' : False, 'name' : 'New Name', diff --git a/test/unit/managers/test_DatasetManager.py b/test/unit/managers/test_DatasetManager.py index df3dfc4b772..e902bd14510 100644 --- a/test/unit/managers/test_DatasetManager.py +++ b/test/unit/managers/test_DatasetManager.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +# -*- coding: utf-8 -*- """ """ import sys @@ -19,7 +19,8 @@ from galaxy.util.bunch import Bunch import mock from test_ModelManager import BaseTestCase -from galaxy.managers.datasets import DatasetManager +from galaxy.managers.datasets import ( + DatasetManager, DatasetRBACPermissions, DatasetSerializer, DatasetDeserializer ) from galaxy.managers.datasets import DatasetAssociationManager from galaxy.managers.histories import HistoryManager from galaxy.managers.hdas import HDAManager @@ -36,118 +37,275 @@ class DatasetManagerTestCase( BaseTestCase ): def set_up_managers( self ): super( DatasetManagerTestCase, self ).set_up_managers() - self.dataset_mgr = DatasetManager( self.app ) + self.dataset_manager = DatasetManager( self.app ) def test_create( self ): self.log( "should be able to create a new Dataset" ) - dataset1 = self.dataset_mgr.create( self.trans ) + dataset1 = self.dataset_manager.create() self.assertIsInstance( dataset1, model.Dataset ) self.assertEqual( dataset1, self.trans.sa_session.query( model.Dataset ).get( dataset1.id ) ) def test_base( self ): - dataset1 = self.dataset_mgr.create( self.trans ) - dataset2 = self.dataset_mgr.create( self.trans ) + dataset1 = self.dataset_manager.create() + dataset2 = self.dataset_manager.create() self.log( "should be able to query" ) datasets = self.trans.sa_session.query( model.Dataset ).all() - self.assertEqual( self.dataset_mgr.list( self.trans ), datasets ) - self.assertEqual( self.dataset_mgr.one( self.trans, filters=( model.Dataset.id == dataset1.id ) ), dataset1 ) - self.assertEqual( self.dataset_mgr.by_id( self.trans, dataset1.id ), dataset1 ) - self.assertEqual( self.dataset_mgr.by_ids( self.trans, [ dataset2.id, dataset1.id ] ), [ dataset2, dataset1 ] ) + self.assertEqual( self.dataset_manager.list(), datasets ) + self.assertEqual( self.dataset_manager.one( filters=( model.Dataset.id == dataset1.id ) ), dataset1 ) + self.assertEqual( self.dataset_manager.by_id( dataset1.id ), dataset1 ) + self.assertEqual( self.dataset_manager.by_ids( [ dataset2.id, dataset1.id ] ), [ dataset2, dataset1 ] ) self.log( "should be able to limit and offset" ) - self.assertEqual( self.dataset_mgr.list( self.trans, limit=1 ), datasets[0:1] ) - self.assertEqual( self.dataset_mgr.list( self.trans, offset=1 ), datasets[1:] ) - self.assertEqual( self.dataset_mgr.list( self.trans, limit=1, offset=1 ), datasets[1:2] ) + self.assertEqual( self.dataset_manager.list( limit=1 ), datasets[0:1] ) + self.assertEqual( self.dataset_manager.list( offset=1 ), datasets[1:] ) + self.assertEqual( self.dataset_manager.list( limit=1, offset=1 ), datasets[1:2] ) - self.assertEqual( self.dataset_mgr.list( self.trans, limit=0 ), [] ) - self.assertEqual( self.dataset_mgr.list( self.trans, offset=3 ), [] ) + self.assertEqual( self.dataset_manager.list( limit=0 ), [] ) + self.assertEqual( self.dataset_manager.list( offset=3 ), [] ) self.log( "should be able to order" ) - self.assertEqual( self.dataset_mgr.list( self.trans, order_by=sqlalchemy.desc( model.Dataset.create_time ) ), + self.assertEqual( self.dataset_manager.list( order_by=sqlalchemy.desc( model.Dataset.create_time ) ), [ dataset2, dataset1 ] ) def test_delete( self ): - item1 = self.dataset_mgr.create( self.trans ) + item1 = self.dataset_manager.create() - self.log( "should be able to delete and undelete an hda" ) + self.log( "should be able to delete and undelete a dataset" ) self.assertFalse( item1.deleted ) - self.assertEqual( self.dataset_mgr.delete( self.trans, item1 ), item1 ) + self.assertEqual( self.dataset_manager.delete( item1 ), item1 ) self.assertTrue( item1.deleted ) - self.assertEqual( self.dataset_mgr.undelete( self.trans, item1 ), item1 ) + self.assertEqual( self.dataset_manager.undelete( item1 ), item1 ) self.assertFalse( item1.deleted ) def test_purge_allowed( self ): self.trans.app.config.allow_user_dataset_purge = True - item1 = self.dataset_mgr.create( self.trans ) + item1 = self.dataset_manager.create() - self.log( "should purge an hda if config does allow" ) + self.log( "should purge a dataset if config does allow" ) self.assertFalse( item1.purged ) - self.assertEqual( self.dataset_mgr.purge( self.trans, item1 ), item1 ) + self.assertEqual( self.dataset_manager.purge( item1 ), item1 ) self.assertTrue( item1.purged ) + self.log( "should delete a dataset when purging" ) + self.assertTrue( item1.deleted ) + def test_purge_not_allowed( self ): self.trans.app.config.allow_user_dataset_purge = False - item1 = self.dataset_mgr.create( self.trans ) + item1 = self.dataset_manager.create() - self.log( "should raise an error when purging an hda if config does not allow" ) + self.log( "should raise an error when purging a dataset if config does not allow" ) self.assertFalse( item1.purged ) - self.assertRaises( exceptions.ConfigDoesNotAllowException, self.dataset_mgr.purge, self.trans, item1 ) + self.assertRaises( exceptions.ConfigDoesNotAllowException, self.dataset_manager.purge, item1 ) self.assertFalse( item1.purged ) - ##TODO: I'm unclear as to how these work, so I'm kicking this down the road a bit.... - #def test_access_permission( self ): - # owner = self.user_mgr.create( self.trans, **user2_data ) - # dataset = self.dataset_mgr.create( self.trans ) - # # giving one user access permission makes it non-public, removing access for anyone else - # self.dataset_mgr.give_access_permission( self.trans, dataset, owner ) - # - # user3 = self.user_mgr.create( self.trans, **user3_data ) - # user4 = self.user_mgr.create( self.trans, - # email='user4@user4.user4', username='user4', password=default_password ) - # - # self.assertTrue( self.dataset_mgr.has_access_permission( self.trans, dataset, owner ) ) - # self.assertFalse( self.dataset_mgr.has_access_permission( self.trans, dataset, user3 ) ) - # self.assertFalse( self.dataset_mgr.has_access_permission( self.trans, dataset, user4 ) ) - # - # # should be able to progressively add more roles without removing the previous - # self.dataset_mgr.give_access_permission( self.trans, dataset, user3 ) - # self.assertTrue( self.dataset_mgr.has_access_permission( self.trans, dataset, user3 ) ) - # self.assertTrue( self.dataset_mgr.has_access_permission( self.trans, dataset, owner ) ) - # self.assertFalse( self.dataset_mgr.has_access_permission( self.trans, dataset, user4 ) ) - # - # #self.assertTrue( self.dataset_mgr.is_accessible( self.trans, dataset, owner ) ) - # #self.assertFalse( self.dataset_mgr.is_accessible( self.trans, dataset, non_owner ) ) + def test_create_with_no_permissions( self ): + self.log( "should be able to create a new Dataset without any permissions" ) + dataset = self.dataset_manager.create() - def test_accessible( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) + permissions = self.dataset_manager.permissions.get( dataset ) + self.assertIsInstance( permissions, tuple ) + self.assertEqual( len( permissions ), 2 ) + manage_permissions, access_permissions = permissions + self.assertEqual( manage_permissions, [] ) + self.assertEqual( access_permissions, [] ) - dataset = self.dataset_mgr.create( self.trans ) + user3 = self.user_manager.create( **user3_data ) + self.log( "a dataset without permissions shouldn't be manageable to just anyone" ) + self.assertFalse( self.dataset_manager.permissions.manage.is_permitted( dataset, user3 ) ) + self.log( "a dataset without permissions should be accessible" ) + self.assertTrue( self.dataset_manager.permissions.access.is_permitted( dataset, user3 ) ) - self.log( "(by default, dataset permissions are lax) should be accessible to all" ) - for user in self.user_mgr.list( self.trans ): - self.assertTrue( self.dataset_mgr.is_accessible( self.trans, dataset, user ) ) + def test_create_public_dataset( self ): + self.log( "should be able to create a new Dataset and give it some permissions that actually, you know, might work if there's any justice in this universe" ) + owner = self.user_manager.create( **user2_data ) + owner_private_role = self.user_manager.private_role( owner ) + dataset = self.dataset_manager.create( manage_roles=[ owner_private_role ] ) + + permissions = self.dataset_manager.permissions.get( dataset ) + self.assertIsInstance( permissions, tuple ) + self.assertEqual( len( permissions ), 2 ) + manage_permissions, access_permissions = permissions + self.assertIsInstance( manage_permissions, list ) + self.assertIsInstance( manage_permissions[0], model.DatasetPermissions ) + self.assertEqual( access_permissions, [] ) + + user3 = self.user_manager.create( **user3_data ) + self.log( "a public dataset should be manageable to it's owner" ) + self.assertTrue( self.dataset_manager.permissions.manage.is_permitted( dataset, owner ) ) + self.log( "a public dataset shouldn't be manageable to just anyone" ) + self.assertFalse( self.dataset_manager.permissions.manage.is_permitted( dataset, user3 ) ) + self.log( "a public dataset should be accessible" ) + self.assertTrue( self.dataset_manager.permissions.access.is_permitted( dataset, user3 ) ) + + def test_create_private_dataset( self ): + self.log( "should be able to create a new Dataset and give it some permissions that actually, you know, might work if there's any justice in this universe" ) + owner = self.user_manager.create( **user2_data ) + owner_private_role = self.user_manager.private_role( owner ) + dataset = self.dataset_manager.create( + manage_roles=[ owner_private_role ], access_roles=[ owner_private_role ] ) + + permissions = self.dataset_manager.permissions.get( dataset ) + self.assertIsInstance( permissions, tuple ) + self.assertEqual( len( permissions ), 2 ) + manage_permissions, access_permissions = permissions + self.assertIsInstance( manage_permissions, list ) + self.assertIsInstance( manage_permissions[0], model.DatasetPermissions ) + self.assertIsInstance( access_permissions, list ) + self.assertIsInstance( access_permissions[0], model.DatasetPermissions ) + + self.log( "a private dataset should be manageable by it's owner" ) + self.assertTrue( self.dataset_manager.permissions.manage.is_permitted( dataset, owner ) ) + self.log( "a private dataset should be accessible to it's owner" ) + self.assertTrue( self.dataset_manager.permissions.access.is_permitted( dataset, owner ) ) + + user3 = self.user_manager.create( **user3_data ) + self.log( "a private dataset shouldn't be manageable to just anyone" ) + self.assertFalse( self.dataset_manager.permissions.manage.is_permitted( dataset, user3 ) ) + self.log( "a private dataset shouldn't be accessible to just anyone" ) + self.assertFalse( self.dataset_manager.permissions.access.is_permitted( dataset, user3 ) ) # ============================================================================= -class DatasetAssociationManagerTestCase( BaseTestCase ): +class DatasetRBACPermissionsTestCase( BaseTestCase ): def set_up_managers( self ): - super( DatasetAssociationManagerTestCase, self ).set_up_managers() - self.dataset_mgr = DatasetAssociationManager( self.app ) - self.hda_mgr = HDAManager( self.app ) - self.history_mgr = HistoryManager( self.app ) + super( DatasetRBACPermissionsTestCase, self ).set_up_managers() + self.dataset_manager = DatasetManager( self.app ) - #def test_metadata( self ): - # owner = self.user_mgr.create( self.trans, **user2_data ) - # history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) - # dataset1 = self.dataset_mgr.create( self.trans ) + # def test_manage( self ): + # self.log( "should be able to create a new Dataset" ) + # dataset1 = self.dataset_manager.create() + # self.assertIsInstance( dataset1, model.Dataset ) + # self.assertEqual( dataset1, self.app.model.context.query( model.Dataset ).get( dataset1.id ) ) # - # self.log( "should be able to create a new HDA with a specified history and dataset" ) - # hda1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1, hid=1 ) +# ============================================================================= +# web.url_for doesn't work well in the framework +testable_url_for = lambda *a, **k: '(fake url): %s, %s' % ( a, k ) +DatasetSerializer.url_for = staticmethod( testable_url_for ) + +class DatasetSerializerTestCase( BaseTestCase ): + + def set_up_managers( self ): + super( DatasetSerializerTestCase, self ).set_up_managers() + self.dataset_manager = DatasetManager( self.app ) + self.dataset_serializer = DatasetSerializer( self.app ) + + def test_views( self ): + dataset = self.dataset_manager.create() + + self.log( 'should have a summary view' ) + summary_view = self.dataset_serializer.serialize_to_view( dataset, view='summary' ) + self.assertKeys( summary_view, self.dataset_serializer.views[ 'summary' ] ) + + self.log( 'should have the summary view as default view' ) + default_view = self.dataset_serializer.serialize_to_view( dataset, default_view='summary' ) + self.assertKeys( summary_view, self.dataset_serializer.views[ 'summary' ] ) + + self.log( 'should have a serializer for all serializable keys' ) + for key in self.dataset_serializer.serializable_keyset: + instantiated_attribute = getattr( dataset, key, None ) + if not ( ( key in self.dataset_serializer.serializers ) + or ( isinstance( instantiated_attribute, self.TYPES_NEEDING_NO_SERIALIZERS ) ) ): + self.fail( 'no serializer for: %s (%s)' % ( key, instantiated_attribute ) ) + else: + self.assertTrue( True, 'all serializable keys have a serializer' ) + + def test_views_and_keys( self ): + dataset = self.dataset_manager.create() + + self.log( 'should be able to use keys with views' ) + serialized = self.dataset_serializer.serialize_to_view( dataset, + view='summary', keys=[ 'permissions' ] ) + self.assertKeys( serialized, + self.dataset_serializer.views[ 'summary' ] + [ 'permissions' ] ) + + self.log( 'should be able to use keys on their own' ) + serialized = self.dataset_serializer.serialize_to_view( dataset, + keys=[ 'purgable', 'file_size' ] ) + self.assertKeys( serialized, [ 'purgable', 'file_size' ] ) + + def test_serialize_permissions( self ): + dataset = self.dataset_manager.create() + self.log( 'serialized permissions should be well formed' ) + + def test_serializers( self ): + user2 = self.user_manager.create( **user2_data ) + dataset = self.dataset_manager.create() + all_keys = list( self.dataset_serializer.serializable_keyset ) + serialized = self.dataset_serializer.serialize( dataset, all_keys ) + + self.log( 'everything serialized should be of the proper type' ) + self.assertEncodedId( serialized[ 'id' ] ) + self.assertDate( serialized[ 'create_time' ] ) + self.assertDate( serialized[ 'update_time' ] ) + + self.assertUUID( serialized[ 'uuid' ] ) + self.assertIsInstance( serialized[ 'state' ], basestring ) + self.assertIsInstance( serialized[ 'deleted' ], bool ) + self.assertIsInstance( serialized[ 'purged' ], bool ) + self.assertIsInstance( serialized[ 'purgable' ], bool ) + + # # TODO: no great way to do these with mocked dataset + # self.assertIsInstance( serialized[ 'file_size' ], int ) + # self.assertIsInstance( serialized[ 'total_size' ], int ) + + self.log( 'serialized should jsonify well' ) + self.assertIsJsonifyable( serialized ) + + +# ============================================================================= +class DatasetDeserializerTestCase( BaseTestCase ): + + def set_up_managers( self ): + super( DatasetDeserializerTestCase, self ).set_up_managers() + self.dataset_manager = DatasetManager( self.app ) + self.dataset_deserializer = DatasetDeserializer( self.app ) + + def test_deserialize_delete( self ): + dataset = self.dataset_manager.create() + + self.log( 'should raise when deserializing deleted from non-bool' ) + self.assertFalse( dataset.deleted ) + self.assertRaises( exceptions.RequestParameterInvalidException, + self.dataset_deserializer.deserialize, dataset, data={ 'deleted': None } ) + self.assertFalse( dataset.deleted ) + self.log( 'should be able to deserialize deleted from True' ) + self.dataset_deserializer.deserialize( dataset, data={ 'deleted': True } ) + self.assertTrue( dataset.deleted ) + self.log( 'should be able to reverse by deserializing deleted from False' ) + self.dataset_deserializer.deserialize( dataset, data={ 'deleted': False } ) + self.assertFalse( dataset.deleted ) + + def test_deserialize_purge( self ): + dataset = self.dataset_manager.create() + + self.log( 'should raise when deserializing purged from non-bool' ) + self.assertRaises( exceptions.RequestParameterInvalidException, + self.dataset_deserializer.deserialize, dataset, data={ 'purged': None } ) + self.assertFalse( dataset.purged ) + self.log( 'should be able to deserialize purged from True' ) + self.dataset_deserializer.deserialize( dataset, data={ 'purged': True } ) + self.assertTrue( dataset.purged ) + # TODO: should this raise an error? + self.log( 'should NOT be able to deserialize purged from False (will remain True)' ) + self.dataset_deserializer.deserialize( dataset, data={ 'purged': False } ) + self.assertTrue( dataset.purged ) + + # def test_deserialize_permissions( self ): + # pass + + +# ============================================================================= +# NOTE: that we test the DatasetAssociation* classes in either test_HDAManager or test_LDAManager +# (as part of those subclasses): +# DatasetAssociationManager, +# DatasetAssociationSerializer, +# DatasetAssociationDeserializer, +# DatasetAssociationFilterParser + # ============================================================================= if __name__ == '__main__': # or more generally, nosetests test_resourcemanagers.py -s -v diff --git a/test/unit/managers/test_HDAManager.py b/test/unit/managers/test_HDAManager.py index 1911d3a74b4..6e7d51c17da 100644 --- a/test/unit/managers/test_HDAManager.py +++ b/test/unit/managers/test_HDAManager.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import sys import os import pprint @@ -18,7 +19,7 @@ import mock from test_ModelManager import BaseTestCase from galaxy.managers.histories import HistoryManager from galaxy.managers.datasets import DatasetManager -from galaxy.managers.hdas import HDAManager +from galaxy.managers import hdas # ============================================================================= @@ -26,52 +27,61 @@ default_password = '123456' user2_data = dict( email='user2@user2.user2', username='user2', password=default_password ) user3_data = dict( email='user3@user3.user3', username='user3', password=default_password ) - -# ============================================================================= -class HDAManagerTestCase( BaseTestCase ): +class HDATestCase( BaseTestCase ): def set_up_managers( self ): - super( HDAManagerTestCase, self ).set_up_managers() - self.history_mgr = HistoryManager( self.app ) - self.dataset_mgr = DatasetManager( self.app ) - self.hda_mgr = HDAManager( self.app ) + super( HDATestCase, self ).set_up_managers() + self.hda_manager = hdas.HDAManager( self.app ) + self.history_manager = HistoryManager( self.app ) + self.dataset_manager = DatasetManager( self.app ) + + def _create_vanilla_hda( self, user_data=None ): + user_data = user_data or user2_data + owner = self.user_manager.create( **user_data ) + history1 = self.history_manager.create( name='history1', user=owner ) + dataset1 = self.dataset_manager.create() + return self.hda_manager.create( history=history1, dataset=dataset1 ) + + +# ============================================================================= +class HDAManagerTestCase( HDATestCase ): def test_base( self ): hda_model = model.HistoryDatasetAssociation - owner = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) - hda1 = self.hda_mgr.create( self.trans, history=history1, hid=1 ) - hda2 = self.hda_mgr.create( self.trans, history=history1, hid=2 ) - hda3 = self.hda_mgr.create( self.trans, history=history1, hid=3 ) + owner = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=owner ) + hda1 = self.hda_manager.create( history=history1, hid=1 ) + hda2 = self.hda_manager.create( history=history1, hid=2 ) + hda3 = self.hda_manager.create( history=history1, hid=3 ) self.log( "should be able to query" ) hdas = self.trans.sa_session.query( hda_model ).all() - self.assertEqual( self.hda_mgr.list( self.trans ), hdas ) - self.assertEqual( self.hda_mgr.one( self.trans, filters=( hda_model.id == hda1.id ) ), hda1 ) - self.assertEqual( self.hda_mgr.by_id( self.trans, hda1.id ), hda1 ) - self.assertEqual( self.hda_mgr.by_ids( self.trans, [ hda2.id, hda1.id ] ), [ hda2, hda1 ] ) + self.assertEqual( self.hda_manager.list(), hdas ) + self.assertEqual( self.hda_manager.one( filters=( hda_model.id == hda1.id ) ), hda1 ) + self.assertEqual( self.hda_manager.by_id( hda1.id ), hda1 ) + self.assertEqual( self.hda_manager.by_ids( [ hda2.id, hda1.id ] ), [ hda2, hda1 ] ) self.log( "should be able to limit and offset" ) - self.assertEqual( self.hda_mgr.list( self.trans, limit=1 ), hdas[0:1] ) - self.assertEqual( self.hda_mgr.list( self.trans, offset=1 ), hdas[1:] ) - self.assertEqual( self.hda_mgr.list( self.trans, limit=1, offset=1 ), hdas[1:2] ) + self.assertEqual( self.hda_manager.list( limit=1 ), hdas[0:1] ) + self.assertEqual( self.hda_manager.list( offset=1 ), hdas[1:] ) + self.assertEqual( self.hda_manager.list( limit=1, offset=1 ), hdas[1:2] ) - self.assertEqual( self.hda_mgr.list( self.trans, limit=0 ), [] ) - self.assertEqual( self.hda_mgr.list( self.trans, offset=3 ), [] ) + self.assertEqual( self.hda_manager.list( limit=0 ), [] ) + self.assertEqual( self.hda_manager.list( offset=3 ), [] ) self.log( "should be able to order" ) - self.assertEqual( self.hda_mgr.list( self.trans, order_by=sqlalchemy.desc( hda_model.create_time ) ), + self.assertEqual( self.hda_manager.list( order_by=sqlalchemy.desc( hda_model.create_time ) ), [ hda3, hda2, hda1 ] ) def test_create( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) + owner = self.user_manager.create( **user2_data ) + non_owner = self.user_manager.create( **user3_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) - dataset1 = self.dataset_mgr.create( self.trans ) + history1 = self.history_manager.create( name='history1', user=owner ) + dataset1 = self.dataset_manager.create() self.log( "should be able to create a new HDA with a specified history and dataset" ) - hda1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1 ) + hda1 = self.hda_manager.create( history=history1, dataset=dataset1 ) self.assertIsInstance( hda1, model.HistoryDatasetAssociation ) self.assertEqual( hda1, self.trans.sa_session.query( model.HistoryDatasetAssociation ).get( hda1.id ) ) self.assertEqual( hda1.history, history1 ) @@ -79,27 +89,27 @@ class HDAManagerTestCase( BaseTestCase ): self.assertEqual( hda1.hid, 1 ) self.log( "should be able to create a new HDA with only a specified history and no dataset" ) - hda2 = self.hda_mgr.create( self.trans, history=history1 ) + hda2 = self.hda_manager.create( history=history1 ) self.assertIsInstance( hda2, model.HistoryDatasetAssociation ) self.assertIsInstance( hda2.dataset, model.Dataset ) self.assertEqual( hda2.history, history1 ) self.assertEqual( hda2.hid, 2 ) self.log( "should be able to create a new HDA with no history and no dataset" ) - hda3 = self.hda_mgr.create( self.trans, hid=None ) + hda3 = self.hda_manager.create( hid=None ) self.assertIsInstance( hda3, model.HistoryDatasetAssociation ) self.assertIsInstance( hda3.dataset, model.Dataset, msg="dataset will be auto created" ) self.assertIsNone( hda3.history, msg="history will be None" ) self.assertEqual( hda3.hid, None, msg="should allow setting hid to None (or any other value)" ) def test_copy_from_hda( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) - dataset1 = self.dataset_mgr.create( self.trans ) - hda1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1 ) + owner = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=owner ) + dataset1 = self.dataset_manager.create() + hda1 = self.hda_manager.create( history=history1, dataset=dataset1 ) self.log( "should be able to copy an HDA" ) - hda2 = self.hda_mgr.copy( self.trans, hda1, history=history1 ) + hda2 = self.hda_manager.copy( hda1, history=history1 ) self.assertIsInstance( hda2, model.HistoryDatasetAssociation ) self.assertEqual( hda2, self.trans.sa_session.query( model.HistoryDatasetAssociation ).get( hda2.id ) ) self.assertEqual( hda2.name, hda1.name ) @@ -108,108 +118,475 @@ class HDAManagerTestCase( BaseTestCase ): self.assertNotEqual( hda2, hda1 ) #def test_copy_from_ldda( self ): - # owner = self.user_mgr.create( self.trans, **user2_data ) - # history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) + # owner = self.user_manager.create( **user2_data ) + # history1 = self.history_manager.create( name='history1', user=owner ) # # self.log( "should be able to copy an HDA" ) - # hda2 = self.hda_mgr.copy_ldda( self.trans, history1, hda1 ) + # hda2 = self.hda_manager.copy_ldda( history1, hda1 ) def test_delete( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) - dataset1 = self.dataset_mgr.create( self.trans ) - item1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1 ) + owner = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=owner ) + dataset1 = self.dataset_manager.create() + item1 = self.hda_manager.create( history=history1, dataset=dataset1 ) self.log( "should be able to delete and undelete an hda" ) self.assertFalse( item1.deleted ) - self.assertEqual( self.hda_mgr.delete( self.trans, item1 ), item1 ) + self.assertEqual( self.hda_manager.delete( item1 ), item1 ) self.assertTrue( item1.deleted ) - self.assertEqual( self.hda_mgr.undelete( self.trans, item1 ), item1 ) + self.assertEqual( self.hda_manager.undelete( item1 ), item1 ) self.assertFalse( item1.deleted ) def test_purge_allowed( self ): self.trans.app.config.allow_user_dataset_purge = True - owner = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) - dataset1 = self.dataset_mgr.create( self.trans ) - item1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1 ) + owner = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=owner ) + dataset1 = self.dataset_manager.create() + item1 = self.hda_manager.create( history=history1, dataset=dataset1 ) self.log( "should purge an hda if config does allow" ) self.assertFalse( item1.purged ) - self.assertEqual( self.hda_mgr.purge( self.trans, item1 ), item1 ) + self.assertEqual( self.hda_manager.purge( item1 ), item1 ) self.assertTrue( item1.purged ) def test_purge_not_allowed( self ): self.trans.app.config.allow_user_dataset_purge = False - owner = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) - dataset1 = self.dataset_mgr.create( self.trans ) - item1 = self.hda_mgr.create( self.trans, history=history1, dataset=dataset1 ) + owner = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=owner ) + dataset1 = self.dataset_manager.create() + item1 = self.hda_manager.create( history=history1, dataset=dataset1 ) self.log( "should raise an error when purging an hda if config does not allow" ) self.assertFalse( item1.purged ) - self.assertRaises( exceptions.ConfigDoesNotAllowException, self.hda_mgr.purge, self.trans, item1 ) + self.assertRaises( exceptions.ConfigDoesNotAllowException, self.hda_manager.purge, item1 ) self.assertFalse( item1.purged ) def test_ownable( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) + owner = self.user_manager.create( **user2_data ) + non_owner = self.user_manager.create( **user3_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) - dataset1 = self.dataset_mgr.create( self.trans ) - item1 = self.hda_mgr.create( self.trans, history1, dataset1 ) + history1 = self.history_manager.create( name='history1', user=owner ) + dataset1 = self.dataset_manager.create() + item1 = self.hda_manager.create( history1, dataset1 ) self.log( "should be able to poll whether a given user owns an item" ) - self.assertTrue( self.hda_mgr.is_owner( self.trans, item1, owner ) ) - self.assertFalse( self.hda_mgr.is_owner( self.trans, item1, non_owner ) ) + self.assertTrue( self.hda_manager.is_owner( item1, owner ) ) + self.assertFalse( self.hda_manager.is_owner( item1, non_owner ) ) self.log( "should raise an error when checking ownership with non-owner" ) self.assertRaises( exceptions.ItemOwnershipException, - self.hda_mgr.error_unless_owner, self.trans, item1, non_owner ) + self.hda_manager.error_unless_owner, item1, non_owner ) self.log( "should raise an error when checking ownership with anonymous" ) self.assertRaises( exceptions.ItemOwnershipException, - self.hda_mgr.error_unless_owner, self.trans, item1, None ) + self.hda_manager.error_unless_owner, item1, None ) self.log( "should not raise an error when checking ownership with owner" ) - self.assertEqual( self.hda_mgr.error_unless_owner( self.trans, item1, owner ), item1 ) + self.assertEqual( self.hda_manager.error_unless_owner( item1, owner ), item1 ) self.log( "should not raise an error when checking ownership with admin" ) - self.assertEqual( self.hda_mgr.error_unless_owner( self.trans, item1, self.admin_user ), item1 ) + self.assertEqual( self.hda_manager.error_unless_owner( item1, self.admin_user ), item1 ) def test_accessible( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) + owner = self.user_manager.create( **user2_data ) + non_owner = self.user_manager.create( **user3_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=owner ) - dataset1 = self.dataset_mgr.create( self.trans ) - item1 = self.hda_mgr.create( self.trans, history1, dataset1 ) + history1 = self.history_manager.create( name='history1', user=owner ) + dataset1 = self.dataset_manager.create() + item1 = self.hda_manager.create( history1, dataset1 ) self.log( "(by default, dataset permissions are lax) should be accessible to all" ) - for user in self.user_mgr.list( self.trans ): - self.assertTrue( self.hda_mgr.is_accessible( self.trans, item1, user ) ) + for user in self.user_manager.list(): + self.assertTrue( self.hda_manager.is_accessible( item1, user ) ) - #TODO: set perms on underlying dataset and then test accessible + self.log( "after setting a dataset to private (one user) permissions, access should be allowed for that user" ) + # for this test, set restrictive access permissions + self.dataset_manager.permissions.set_private_to_one_user( dataset1, owner ) + accessible = self.hda_manager.get_accessible( item1.id, owner, current_history=self.trans.history ) + self.assertEqual( accessible, item1 ) - def test_anon( self ): + self.log( "after setting a dataset to private (one user) permissions, " + + "access should be not allowed for other users" ) + self.assertRaises( exceptions.ItemAccessibilityException, + self.hda_manager.get_accessible, item1.id, non_owner, current_history=self.trans.history ) + + self.log( "a copy of a restricted dataset in another users history should be inaccessible even to " + + "the histories owner" ) + history2 = self.history_manager.create( name='history2', user=non_owner ) + self.trans.set_history( history2 ) + item2 = self.hda_manager.copy( item1, history=history2 ) + self.assertIsInstance( item2, model.HistoryDatasetAssociation ) + self.assertRaises( exceptions.ItemAccessibilityException, + self.hda_manager.get_accessible, item2.id, non_owner, current_history=self.trans.history ) + + self.log( "a restricted dataset cannot be accessed by anonymous users" ) + anon_user = None + self.trans.set_user( anon_user ) + history3 = self.history_manager.create( name='anon_history', user=anon_user ) + self.trans.set_history( history2 ) + self.assertRaises( exceptions.ItemAccessibilityException, + self.hda_manager.get_accessible, item1.id, anon_user, current_history=self.trans.history ) + + def test_anon_ownership( self ): anon_user = None self.trans.set_user( anon_user ) - history1 = self.history_mgr.create( self.trans, name='anon_history', user=anon_user ) + history1 = self.history_manager.create( name='anon_history', user=anon_user ) self.trans.set_history( history1 ) - dataset1 = self.dataset_mgr.create( self.trans ) - item1 = self.hda_mgr.create( self.trans, history1, dataset1 ) + dataset1 = self.dataset_manager.create() + item1 = self.hda_manager.create( history1, dataset1 ) - self.log( "should not raise an error when checking ownership/access on anonymous' own dataset" ) - self.assertTrue( self.hda_mgr.is_accessible( self.trans, item1, anon_user ) ) - self.assertEqual( self.hda_mgr.error_unless_owner( self.trans, item1, anon_user ), item1 ) + self.log( "should not raise an error when checking ownership on anonymous' own dataset" ) + # need to pass the current history for comparison + self.assertTrue( self.hda_manager.is_owner( item1, anon_user, current_history=self.trans.history ) ) + item = self.hda_manager.error_unless_owner( item1, anon_user, current_history=self.trans.history ) + self.assertEqual( item, item1 ) + item = self.hda_manager.get_owned( item1.id, anon_user, current_history=self.trans.history ) + self.assertEqual( item, item1 ) self.log( "should raise an error when checking ownership on anonymous' dataset with other user" ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) + non_owner = self.user_manager.create( **user3_data ) + self.assertFalse( self.hda_manager.is_owner( item1, non_owner ) ) self.assertRaises( exceptions.ItemOwnershipException, - self.hda_mgr.error_unless_owner, self.trans, item1, non_owner ) + self.hda_manager.error_unless_owner, item1, non_owner ) + self.assertRaises( exceptions.ItemOwnershipException, + self.hda_manager.get_owned, item1.id, non_owner ) + + def test_anon_accessibility( self ): + anon_user = None + self.trans.set_user( anon_user ) + + history1 = self.history_manager.create( name='anon_history', user=anon_user ) + self.trans.set_history( history1 ) + dataset1 = self.dataset_manager.create() + item1 = self.hda_manager.create( history1, dataset1 ) + + # datasets are public by default + self.assertTrue( self.hda_manager.is_accessible( item1, anon_user ) ) + # for this test, set restrictive access permissions + dataset_owner = self.user_manager.create( **user3_data ) + self.dataset_manager.permissions.set_private_to_one_user( dataset1, dataset_owner ) + + self.log( "anonymous users should not be able to access datasets within their own histories if " + + "permissions do not allow" ) + self.assertFalse( self.hda_manager.is_accessible( item1, anon_user ) ) + self.assertRaises( exceptions.ItemAccessibilityException, + self.hda_manager.error_unless_accessible, item1, anon_user ) + + self.log( "those users with access permissions should still be allowed access to datasets " + + "within anon users' histories" ) + self.assertTrue( self.hda_manager.is_accessible( item1, dataset_owner ) ) + + def test_error_if_uploading( self ): + hda = self._create_vanilla_hda() + + hda.state = model.Dataset.states.OK + self.log( "should not raise an error when calling error_if_uploading and in a non-uploading state" ) + self.assertEqual( self.hda_manager.error_if_uploading( hda ), hda ) + + hda.state = model.Dataset.states.UPLOAD + self.log( "should raise an error when calling error_if_uploading and in the uploading state" ) + self.assertRaises( exceptions.Conflict, + self.hda_manager.error_if_uploading, hda ) + + def test_data_conversion_status( self ): + hda = self._create_vanilla_hda() + + self.log( "data conversion status should reflect state" ) + self.assertEqual( self.hda_manager.data_conversion_status( None ), + hda.conversion_messages.NO_DATA ) + hda.state = model.Dataset.states.ERROR + self.assertEqual( self.hda_manager.data_conversion_status( hda ), + hda.conversion_messages.ERROR ) + hda.state = model.Dataset.states.QUEUED + self.assertEqual( self.hda_manager.data_conversion_status( hda ), + hda.conversion_messages.PENDING ) + hda.state = model.Dataset.states.OK + self.assertEqual( self.hda_manager.data_conversion_status( hda ), None ) + + # def test_text_data( self ): + + +# ============================================================================= +# web.url_for doesn't work well in the framework +testable_url_for = lambda *a, **k: '(fake url): %s, %s' % ( a, k ) +hdas.HDASerializer.url_for = staticmethod( testable_url_for ) + +class HDASerializerTestCase( HDATestCase ): + + def set_up_managers( self ): + super( HDASerializerTestCase, self ).set_up_managers() + self.hda_serializer = hdas.HDASerializer( self.app ) + + def test_views( self ): + hda = self._create_vanilla_hda() + + self.log( 'should have a summary view' ) + summary_view = self.hda_serializer.serialize_to_view( hda, view='summary' ) + self.assertKeys( summary_view, self.hda_serializer.views[ 'summary' ] ) + + self.log( 'should have the summary view as default view' ) + default_view = self.hda_serializer.serialize_to_view( hda, default_view='summary' ) + self.assertKeys( summary_view, self.hda_serializer.views[ 'summary' ] ) + + # self.log( 'should have a detailed view' ) + # detailed_view = self.hda_serializer.serialize_to_view( hda, view='detailed' ) + # self.assertKeys( detailed_view, self.hda_serializer.views[ 'detailed' ] ) + + # self.log( 'should have a extended view' ) + # extended_view = self.hda_serializer.serialize_to_view( hda, view='extended' ) + # self.assertKeys( extended_view, self.hda_serializer.views[ 'extended' ] ) + + self.log( 'should have a inaccessible view' ) + inaccessible_view = self.hda_serializer.serialize_to_view( hda, view='inaccessible' ) + self.assertKeys( inaccessible_view, self.hda_serializer.views[ 'inaccessible' ] ) + + # skip metadata for this test + def is_metadata( key ): + return ( key == 'metadata' + or key.startswith( 'metadata_' ) ) + + self.log( 'should have a serializer for all serializable keys' ) + for key in self.hda_serializer.serializable_keyset: + instantiated_attribute = getattr( hda, key, None ) + if not ( ( key in self.hda_serializer.serializers ) + or ( isinstance( instantiated_attribute, self.TYPES_NEEDING_NO_SERIALIZERS ) ) + or ( is_metadata( key ) ) ): + self.fail( 'no serializer for: %s (%s)' % ( key, instantiated_attribute ) ) + else: + self.assertTrue( True, 'all serializable keys have a serializer' ) + + def test_views_and_keys( self ): + hda = self._create_vanilla_hda() + + self.log( 'should be able to use keys with views' ) + serialized = self.hda_serializer.serialize_to_view( hda, + view='summary', keys=[ 'uuid' ] ) + self.assertKeys( serialized, + self.hda_serializer.views[ 'summary' ] + [ 'uuid' ] ) + + self.log( 'should be able to use keys on their own' ) + serialized = self.hda_serializer.serialize_to_view( hda, + keys=[ 'file_path', 'visualizations' ] ) + self.assertKeys( serialized, [ 'file_path', 'visualizations' ] ) + + def test_serializers( self ): + hda = self._create_vanilla_hda() + all_keys = list( self.hda_serializer.serializable_keyset ) + serialized = self.hda_serializer.serialize( hda, all_keys ) + + self.log( 'everything serialized should be of the proper type' ) + # base + self.assertEncodedId( serialized[ 'id' ] ) + self.assertDate( serialized[ 'create_time' ] ) + self.assertDate( serialized[ 'update_time' ] ) + + # dataset association + self.assertIsInstance( serialized[ 'dataset' ], dict ) + self.assertEncodedId( serialized[ 'dataset_id' ] ) + self.assertUUID( serialized[ 'uuid' ] ) + self.assertIsInstance( serialized[ 'file_name' ], basestring ) + self.assertIsInstance( serialized[ 'extra_files_path' ], basestring ) + self.assertIsInstance( serialized[ 'permissions' ], dict ) + self.assertIsInstance( serialized[ 'size' ], int ) + self.assertIsInstance( serialized[ 'file_size' ], int ) + self.assertIsInstance( serialized[ 'nice_size' ], basestring ) + # TODO: these should be tested w/copy + self.assertNullableEncodedId( serialized[ 'copied_from_history_dataset_association_id'] ) + self.assertNullableEncodedId( serialized[ 'copied_from_library_dataset_dataset_association_id'] ) + self.assertNullableBasestring( serialized[ 'info' ] ) + self.assertNullableBasestring( serialized[ 'blurb' ] ) + self.assertNullableBasestring( serialized[ 'peek' ] ) + self.assertIsInstance( serialized[ 'meta_files' ], list ) + self.assertNullableEncodedId( serialized[ 'parent_id'] ) + self.assertEqual( serialized[ 'designation' ], None ) + self.assertIsInstance( serialized[ 'genome_build' ], basestring ) + self.assertIsInstance( serialized[ 'data_type' ], basestring ) + + # hda + self.assertEncodedId( serialized[ 'history_id' ] ) + self.assertEqual( serialized[ 'type_id' ], 'dataset-' + serialized[ 'id' ] ) + + self.assertIsInstance( serialized[ 'resubmitted' ], bool ) + self.assertIsInstance( serialized[ 'display_apps' ], list ) + self.assertIsInstance( serialized[ 'display_types' ], list ) + self.assertIsInstance( serialized[ 'visualizations' ], list ) + + # remapped + self.assertNullableBasestring( serialized[ 'misc_info' ] ) + self.assertNullableBasestring( serialized[ 'misc_blurb' ] ) + self.assertNullableBasestring( serialized[ 'file_ext' ] ) + self.assertNullableBasestring( serialized[ 'file_path' ] ) + + # identities + self.assertEqual( serialized[ 'model_class' ], 'HistoryDatasetAssociation' ) + self.assertEqual( serialized[ 'history_content_type' ], 'dataset' ) + self.assertEqual( serialized[ 'hda_ldda' ], 'hda' ) + self.assertEqual( serialized[ 'accessible' ], True ) + self.assertEqual( serialized[ 'api_type' ], 'file' ) + self.assertEqual( serialized[ 'type' ], 'file' ) + + self.assertIsInstance( serialized[ 'url' ], basestring ) + self.assertIsInstance( serialized[ 'urls' ], dict ) + self.assertIsInstance( serialized[ 'download_url' ], basestring ) + + self.log( 'serialized should jsonify well' ) + self.assertIsJsonifyable( serialized ) + + +# ============================================================================= +class HDADeserializerTestCase( HDATestCase ): + + def set_up_managers( self ): + super( HDADeserializerTestCase, self ).set_up_managers() + self.hda_deserializer = hdas.HDADeserializer( self.app ) + + def test_deserialize_delete( self ): + hda = self._create_vanilla_hda() + + self.log( 'should raise when deserializing deleted from non-bool' ) + self.assertFalse( hda.deleted ) + self.assertRaises( exceptions.RequestParameterInvalidException, + self.hda_deserializer.deserialize, hda, { 'deleted': None } ) + self.assertFalse( hda.deleted ) + self.log( 'should be able to deserialize deleted from True' ) + self.hda_deserializer.deserialize( hda, { 'deleted': True } ) + self.assertTrue( hda.deleted ) + self.log( 'should be able to reverse by deserializing deleted from False' ) + self.hda_deserializer.deserialize( hda, { 'deleted': False } ) + self.assertFalse( hda.deleted ) + + def test_deserialize_purge( self ): + hda = self._create_vanilla_hda() + + self.log( 'should raise when deserializing purged from non-bool' ) + self.assertRaises( exceptions.RequestParameterInvalidException, + self.hda_deserializer.deserialize, hda, { 'purged': None } ) + self.assertFalse( hda.purged ) + self.log( 'should be able to deserialize purged from True' ) + self.hda_deserializer.deserialize( hda, { 'purged': True } ) + self.assertTrue( hda.purged ) + # TODO: should this raise an error? + self.log( 'should NOT be able to deserialize purged from False (will remain True)' ) + self.hda_deserializer.deserialize( hda, { 'purged': False } ) + self.assertTrue( hda.purged ) + + def test_deserialize_visible( self ): + hda = self._create_vanilla_hda() + + self.log( 'should raise when deserializing from non-bool' ) + self.assertTrue( hda.visible ) + self.assertRaises( exceptions.RequestParameterInvalidException, + self.hda_deserializer.deserialize, hda, { 'visible': 'None' } ) + self.assertTrue( hda.visible ) + self.log( 'should be able to deserialize from False' ) + self.hda_deserializer.deserialize( hda, { 'visible': False } ) + self.assertFalse( hda.visible ) + self.log( 'should be able to reverse by deserializing from True' ) + self.hda_deserializer.deserialize( hda, { 'visible': True } ) + self.assertTrue( hda.visible ) + + def test_deserialize_genome_build( self ): + hda = self._create_vanilla_hda() + + self.assertIsInstance( hda.dbkey, basestring ) + self.log( 'should deserialize to "?" from None' ) + self.hda_deserializer.deserialize( hda, { 'genome_build': None } ) + self.assertEqual( hda.dbkey, '?' ) + self.log( 'should raise when deserializing from non-string' ) + self.assertRaises( exceptions.RequestParameterInvalidException, + self.hda_deserializer.deserialize, hda, { 'genome_build': 12 } ) + self.log( 'should be able to deserialize from unicode' ) + date_palm = u'نخيل التمر' + self.hda_deserializer.deserialize( hda, { 'genome_build': date_palm } ) + self.assertEqual( hda.dbkey, date_palm ) + self.log( 'should be deserializable from empty string' ) + self.hda_deserializer.deserialize( hda, { 'genome_build': '' } ) + self.assertEqual( hda.dbkey, '' ) + + def test_deserialize_name( self ): + hda = self._create_vanilla_hda() + + self.log( 'should raise when deserializing from non-string' ) + self.assertRaises( exceptions.RequestParameterInvalidException, + self.hda_deserializer.deserialize, hda, { 'name': True } ) + self.log( 'should raise when deserializing from None' ) + self.assertRaises( exceptions.RequestParameterInvalidException, + self.hda_deserializer.deserialize, hda, { 'name': None } ) + # self.log( 'should deserialize to empty string from None' ) + # self.hda_deserializer.deserialize( hda, { 'name': None } ) + # self.assertEqual( hda.name, '' ) + self.log( 'should be able to deserialize from unicode' ) + olive = u'ελιά' + self.hda_deserializer.deserialize( hda, { 'name': olive } ) + self.assertEqual( hda.name, olive ) + self.log( 'should be deserializable from empty string' ) + self.hda_deserializer.deserialize( hda, { 'name': '' } ) + self.assertEqual( hda.name, '' ) + + def test_deserialize_info( self ): + hda = self._create_vanilla_hda() + + self.log( 'should raise when deserializing from non-string' ) + self.assertRaises( exceptions.RequestParameterInvalidException, + self.hda_deserializer.deserialize, hda, { 'info': True } ) + self.log( 'should raise when deserializing from None' ) + self.assertRaises( exceptions.RequestParameterInvalidException, + self.hda_deserializer.deserialize, hda, { 'info': None } ) + self.log( 'should be able to deserialize from unicode' ) + rice = u'飯' + self.hda_deserializer.deserialize( hda, { 'info': rice } ) + self.assertEqual( hda.info, rice ) + self.log( 'should be deserializable from empty string' ) + self.hda_deserializer.deserialize( hda, { 'info': '' } ) + self.assertEqual( hda.info, '' ) + + +# ============================================================================= +class HDAFilterParserTestCase( HDATestCase ): + + def set_up_managers( self ): + super( HDAFilterParserTestCase, self ).set_up_managers() + self.filter_parser = hdas.HDAFilterParser( self.app ) + + def test_parsable( self ): + self.log( 'the following filters should be parsable' ) + # base + self.assertORMFilter( self.filter_parser.parse_filter( 'id', 'in', [ 1, 2 ] ) ) + encoded_id_string = ','.join([ self.app.security.encode_id( id_ ) for id_ in [ 1, 2 ] ] ) + self.assertORMFilter( self.filter_parser.parse_filter( 'encoded_id', 'in', encoded_id_string ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'create_time', 'le', '2015-03-15' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'create_time', 'ge', '2015-03-15' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'update_time', 'le', '2015-03-15' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'update_time', 'ge', '2015-03-15' ) ) + # purgable + self.assertORMFilter( self.filter_parser.parse_filter( 'deleted', 'eq', True ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'purged', 'eq', True ) ) + # dataset asociation + self.assertORMFilter( self.filter_parser.parse_filter( 'name', 'eq', 'wot' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'name', 'contains', 'wot' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'name', 'like', 'wot' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'state', 'eq', 'ok' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'state', 'in', [ 'queued', 'running' ] ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'visible', 'eq', True ) ) + self.assertFnFilter( self.filter_parser.parse_filter( 'genome_build', 'eq', 'wot' ) ) + self.assertFnFilter( self.filter_parser.parse_filter( 'genome_build', 'contains', 'wot' ) ) + self.assertFnFilter( self.filter_parser.parse_filter( 'data_type', 'eq', 'wot' ) ) + self.assertFnFilter( self.filter_parser.parse_filter( 'data_type', 'isinstance', 'wot' ) ) + # taggable + self.assertFnFilter( self.filter_parser.parse_filter( 'tag', 'eq', 'wot' ) ) + self.assertFnFilter( self.filter_parser.parse_filter( 'tag', 'has', 'wot' ) ) + # annotatable + self.assertFnFilter( self.filter_parser.parse_filter( 'annotation', 'has', 'wot' ) ) + + + def test_genome_build_filters( self ): + pass + + def test_data_type_filters( self ): + pass # ============================================================================= diff --git a/test/unit/managers/test_HistoryManager.py b/test/unit/managers/test_HistoryManager.py index db7d777a4df..b727b3dddff 100644 --- a/test/unit/managers/test_HistoryManager.py +++ b/test/unit/managers/test_HistoryManager.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +# -*- coding: utf-8 -*- """ """ import sys @@ -39,14 +39,14 @@ class HistoryManagerTestCase( BaseTestCase ): def set_up_managers( self ): super( HistoryManagerTestCase, self ).set_up_managers() - self.history_mgr = HistoryManager( self.app ) + self.history_manager = HistoryManager( self.app ) def test_base( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - user3 = self.user_mgr.create( self.trans, **user3_data ) + user2 = self.user_manager.create( **user2_data ) + user3 = self.user_manager.create( **user3_data ) self.log( "should be able to create a new history" ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) + history1 = self.history_manager.create( name='history1', user=user2 ) self.assertIsInstance( history1, model.History ) self.assertEqual( history1.name, 'history1' ) self.assertEqual( history1.user, user2 ) @@ -57,7 +57,7 @@ class HistoryManagerTestCase( BaseTestCase ): self.trans.sa_session.query( model.History ).filter( model.History.user == user2 ).one() ) self.log( "should be able to copy a history" ) - history2 = self.history_mgr.copy( self.trans, history1, user=user3 ) + history2 = self.history_manager.copy( history1, user=user3 ) self.assertIsInstance( history2, model.History ) self.assertEqual( history2.user, user3 ) self.assertEqual( history2, self.trans.sa_session.query( model.History ).get( history2.id ) ) @@ -66,184 +66,184 @@ class HistoryManagerTestCase( BaseTestCase ): self.log( "should be able to query" ) histories = self.trans.sa_session.query( model.History ).all() - self.assertEqual( self.history_mgr.one( self.trans, filters=( model.History.id == history1.id ) ), history1 ) - self.assertEqual( self.history_mgr.list( self.trans ), histories ) - self.assertEqual( self.history_mgr.by_id( self.trans, history1.id ), history1 ) - self.assertEqual( self.history_mgr.by_ids( self.trans, [ history2.id, history1.id ] ), [ history2, history1 ] ) + self.assertEqual( self.history_manager.one( filters=( model.History.id == history1.id ) ), history1 ) + self.assertEqual( self.history_manager.list(), histories ) + self.assertEqual( self.history_manager.by_id( history1.id ), history1 ) + self.assertEqual( self.history_manager.by_ids( [ history2.id, history1.id ] ), [ history2, history1 ] ) self.log( "should be able to limit and offset" ) - self.assertEqual( self.history_mgr.list( self.trans, limit=1 ), histories[0:1] ) - self.assertEqual( self.history_mgr.list( self.trans, offset=1 ), histories[1:] ) - self.assertEqual( self.history_mgr.list( self.trans, limit=1, offset=1 ), histories[1:2] ) + self.assertEqual( self.history_manager.list( limit=1 ), histories[0:1] ) + self.assertEqual( self.history_manager.list( offset=1 ), histories[1:] ) + self.assertEqual( self.history_manager.list( limit=1, offset=1 ), histories[1:2] ) - self.assertEqual( self.history_mgr.list( self.trans, limit=0 ), [] ) - self.assertEqual( self.history_mgr.list( self.trans, offset=3 ), [] ) + self.assertEqual( self.history_manager.list( limit=0 ), [] ) + self.assertEqual( self.history_manager.list( offset=3 ), [] ) self.log( "should be able to order" ) - history3 = self.history_mgr.create( self.trans, name="history3", user=user2 ) + history3 = self.history_manager.create( name="history3", user=user2 ) name_first_then_time = ( model.History.name, sqlalchemy.desc( model.History.create_time ) ) - self.assertEqual( self.history_mgr.list( self.trans, order_by=name_first_then_time ), + self.assertEqual( self.history_manager.list( order_by=name_first_then_time ), [ history2, history1, history3 ] ) def test_has_user( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) + owner = self.user_manager.create( **user2_data ) + non_owner = self.user_manager.create( **user3_data ) - item1 = self.history_mgr.create( self.trans, user=owner ) - item2 = self.history_mgr.create( self.trans, user=owner ) - item3 = self.history_mgr.create( self.trans, user=non_owner ) + item1 = self.history_manager.create( user=owner ) + item2 = self.history_manager.create( user=owner ) + item3 = self.history_manager.create( user=non_owner ) self.log( "should be able to list items by user" ) - user_histories = self.history_mgr.by_user( self.trans, owner ) + user_histories = self.history_manager.by_user( owner ) self.assertEqual( user_histories, [ item1, item2 ] ) def test_ownable( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) + owner = self.user_manager.create( **user2_data ) + non_owner = self.user_manager.create( **user3_data ) - item1 = self.history_mgr.create( self.trans, user=owner ) + item1 = self.history_manager.create( user=owner ) self.log( "should be able to poll whether a given user owns an item" ) - self.assertTrue( self.history_mgr.is_owner( self.trans, item1, owner ) ) - self.assertFalse( self.history_mgr.is_owner( self.trans, item1, non_owner ) ) + self.assertTrue( self.history_manager.is_owner( item1, owner ) ) + self.assertFalse( self.history_manager.is_owner( item1, non_owner ) ) self.log( "should raise an error when checking ownership with non-owner" ) self.assertRaises( exceptions.ItemOwnershipException, - self.history_mgr.error_unless_owner, self.trans, item1, non_owner ) + self.history_manager.error_unless_owner, item1, non_owner ) self.assertRaises( exceptions.ItemOwnershipException, - self.history_mgr.get_owned, self.trans, item1.id, non_owner ) + self.history_manager.get_owned, item1.id, non_owner ) self.log( "should not raise an error when checking ownership with owner" ) - self.assertEqual( self.history_mgr.error_unless_owner( self.trans, item1, owner ), item1 ) - self.assertEqual( self.history_mgr.get_owned( self.trans, item1.id, owner ), item1 ) + self.assertEqual( self.history_manager.error_unless_owner( item1, owner ), item1 ) + self.assertEqual( self.history_manager.get_owned( item1.id, owner ), item1 ) self.log( "should not raise an error when checking ownership with admin" ) - self.assertTrue( self.history_mgr.is_owner( self.trans, item1, self.admin_user ) ) - self.assertEqual( self.history_mgr.error_unless_owner( self.trans, item1, self.admin_user ), item1 ) - self.assertEqual( self.history_mgr.get_owned( self.trans, item1.id, self.admin_user ), item1 ) + self.assertTrue( self.history_manager.is_owner( item1, self.admin_user ) ) + self.assertEqual( self.history_manager.error_unless_owner( item1, self.admin_user ), item1 ) + self.assertEqual( self.history_manager.get_owned( item1.id, self.admin_user ), item1 ) def test_accessible( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) - item1 = self.history_mgr.create( self.trans, user=owner ) + owner = self.user_manager.create( **user2_data ) + item1 = self.history_manager.create( user=owner ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) + non_owner = self.user_manager.create( **user3_data ) self.log( "should be inaccessible by default except to owner" ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, item1, owner ) ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, item1, self.admin_user ) ) - self.assertFalse( self.history_mgr.is_accessible( self.trans, item1, non_owner ) ) + self.assertTrue( self.history_manager.is_accessible( item1, owner ) ) + self.assertTrue( self.history_manager.is_accessible( item1, self.admin_user ) ) + self.assertFalse( self.history_manager.is_accessible( item1, non_owner ) ) self.log( "should raise an error when checking accessibility with non-owner" ) self.assertRaises( exceptions.ItemAccessibilityException, - self.history_mgr.error_unless_accessible, self.trans, item1, non_owner ) + self.history_manager.error_unless_accessible, item1, non_owner ) self.assertRaises( exceptions.ItemAccessibilityException, - self.history_mgr.get_accessible, self.trans, item1.id, non_owner ) + self.history_manager.get_accessible, item1.id, non_owner ) self.log( "should not raise an error when checking ownership with owner" ) - self.assertEqual( self.history_mgr.error_unless_accessible( self.trans, item1, owner ), item1 ) - self.assertEqual( self.history_mgr.get_accessible( self.trans, item1.id, owner ), item1 ) + self.assertEqual( self.history_manager.error_unless_accessible( item1, owner ), item1 ) + self.assertEqual( self.history_manager.get_accessible( item1.id, owner ), item1 ) self.log( "should not raise an error when checking ownership with admin" ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, item1, self.admin_user ) ) - self.assertEqual( self.history_mgr.error_unless_accessible( self.trans, item1, self.admin_user ), item1 ) - self.assertEqual( self.history_mgr.get_accessible( self.trans, item1.id, self.admin_user ), item1 ) + self.assertTrue( self.history_manager.is_accessible( item1, self.admin_user ) ) + self.assertEqual( self.history_manager.error_unless_accessible( item1, self.admin_user ), item1 ) + self.assertEqual( self.history_manager.get_accessible( item1.id, self.admin_user ), item1 ) def test_importable( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) + owner = self.user_manager.create( **user2_data ) self.trans.set_user( owner ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) + non_owner = self.user_manager.create( **user3_data ) - item1 = self.history_mgr.create( self.trans, user=owner ) + item1 = self.history_manager.create( user=owner ) self.log( "should not be importable by default" ) self.assertFalse( item1.importable ) self.assertIsNone( item1.slug ) self.log( "should be able to make importable (accessible by link) to all users" ) - accessible = self.history_mgr.make_importable( self.trans, item1 ) + accessible = self.history_manager.make_importable( item1 ) self.assertEqual( accessible, item1 ) self.assertIsNotNone( accessible.slug ) self.assertTrue( accessible.importable ) - for user in self.user_mgr.list( self.trans ): - self.assertTrue( self.history_mgr.is_accessible( self.trans, accessible, user ) ) + for user in self.user_manager.list(): + self.assertTrue( self.history_manager.is_accessible( accessible, user ) ) self.log( "should be able to make non-importable/inaccessible again" ) - inaccessible = self.history_mgr.make_non_importable( self.trans, accessible ) + inaccessible = self.history_manager.make_non_importable( accessible ) self.assertEqual( inaccessible, accessible ) self.assertIsNotNone( inaccessible.slug ) self.assertFalse( inaccessible.importable ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, inaccessible, owner ) ) - self.assertFalse( self.history_mgr.is_accessible( self.trans, inaccessible, non_owner ) ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, inaccessible, self.admin_user ) ) + self.assertTrue( self.history_manager.is_accessible( inaccessible, owner ) ) + self.assertFalse( self.history_manager.is_accessible( inaccessible, non_owner ) ) + self.assertTrue( self.history_manager.is_accessible( inaccessible, self.admin_user ) ) def test_published( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) + owner = self.user_manager.create( **user2_data ) self.trans.set_user( owner ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) + non_owner = self.user_manager.create( **user3_data ) - item1 = self.history_mgr.create( self.trans, user=owner ) + item1 = self.history_manager.create( user=owner ) self.log( "should not be published by default" ) self.assertFalse( item1.published ) self.assertIsNone( item1.slug ) self.log( "should be able to publish (listed publicly) to all users" ) - published = self.history_mgr.publish( self.trans, item1 ) + published = self.history_manager.publish( item1 ) self.assertEqual( published, item1 ) self.assertTrue( published.published ) # note: publishing sets importable to true as well self.assertTrue( published.importable ) self.assertIsNotNone( published.slug ) - for user in self.user_mgr.list( self.trans ): - self.assertTrue( self.history_mgr.is_accessible( self.trans, published, user ) ) + for user in self.user_manager.list(): + self.assertTrue( self.history_manager.is_accessible( published, user ) ) self.log( "should be able to make non-importable/inaccessible again" ) - unpublished = self.history_mgr.unpublish( self.trans, published ) + unpublished = self.history_manager.unpublish( published ) self.assertEqual( unpublished, published ) self.assertFalse( unpublished.published ) # note: unpublishing does not make non-importable, you must explicitly do that separately self.assertTrue( published.importable ) - self.history_mgr.make_non_importable( self.trans, unpublished ) + self.history_manager.make_non_importable( unpublished ) self.assertFalse( published.importable ) # note: slug still remains after unpublishing self.assertIsNotNone( unpublished.slug ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, unpublished, owner ) ) - self.assertFalse( self.history_mgr.is_accessible( self.trans, unpublished, non_owner ) ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, unpublished, self.admin_user ) ) + self.assertTrue( self.history_manager.is_accessible( unpublished, owner ) ) + self.assertFalse( self.history_manager.is_accessible( unpublished, non_owner ) ) + self.assertTrue( self.history_manager.is_accessible( unpublished, self.admin_user ) ) def test_sharable( self ): - owner = self.user_mgr.create( self.trans, **user2_data ) + owner = self.user_manager.create( **user2_data ) self.trans.set_user( owner ) - item1 = self.history_mgr.create( self.trans, user=owner ) + item1 = self.history_manager.create( user=owner ) - non_owner = self.user_mgr.create( self.trans, **user3_data ) - #third_party = self.user_mgr.create( self.trans, **user4_data ) + non_owner = self.user_manager.create( **user3_data ) + #third_party = self.user_manager.create( **user4_data ) self.log( "should be unshared by default" ) - self.assertEqual( self.history_mgr.get_share_assocs( self.trans, item1 ), [] ) + self.assertEqual( self.history_manager.get_share_assocs( item1 ), [] ) self.assertEqual( item1.slug, None ) self.log( "should be able to share with specific users" ) - share_assoc = self.history_mgr.share_with( self.trans, item1, non_owner ) + share_assoc = self.history_manager.share_with( item1, non_owner ) self.assertIsInstance( share_assoc, model.HistoryUserShareAssociation ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, item1, non_owner ) ) + self.assertTrue( self.history_manager.is_accessible( item1, non_owner ) ) self.assertEqual( - len( self.history_mgr.get_share_assocs( self.trans, item1 ) ), 1 ) + len( self.history_manager.get_share_assocs( item1 ) ), 1 ) self.assertEqual( - len( self.history_mgr.get_share_assocs( self.trans, item1, user=non_owner ) ), 1 ) + len( self.history_manager.get_share_assocs( item1, user=non_owner ) ), 1 ) self.assertIsInstance( item1.slug, basestring ) self.log( "should be able to unshare with specific users" ) - share_assoc = self.history_mgr.unshare_with( self.trans, item1, non_owner ) + share_assoc = self.history_manager.unshare_with( item1, non_owner ) self.assertIsInstance( share_assoc, model.HistoryUserShareAssociation ) - self.assertFalse( self.history_mgr.is_accessible( self.trans, item1, non_owner ) ) - self.assertEqual( self.history_mgr.get_share_assocs( self.trans, item1 ), [] ) + self.assertFalse( self.history_manager.is_accessible( item1, non_owner ) ) + self.assertEqual( self.history_manager.get_share_assocs( item1 ), [] ) self.assertEqual( - self.history_mgr.get_share_assocs( self.trans, item1, user=non_owner ), [] ) + self.history_manager.get_share_assocs( item1, user=non_owner ), [] ) #TODO: test slug formation @@ -252,61 +252,64 @@ class HistoryManagerTestCase( BaseTestCase ): self.trans.set_user( anon_user ) self.log( "should not allow access and owner for anon user on a history by another anon user (None)" ) - anon_history1 = self.history_mgr.create( self.trans, user=None ) - self.assertFalse( self.history_mgr.is_owner( self.trans, anon_history1, anon_user ) ) - self.assertFalse( self.history_mgr.is_accessible( self.trans, anon_history1, anon_user ) ) + anon_history1 = self.history_manager.create( user=None ) + # do not set the trans.history! + self.assertFalse( self.history_manager.is_owner( anon_history1, anon_user, current_history=self.trans.history ) ) + self.assertFalse( self.history_manager.is_accessible( anon_history1, anon_user, current_history=self.trans.history ) ) self.log( "should allow access and owner for anon user on a history if it's the session's current history" ) - anon_history2 = self.history_mgr.create( self.trans, user=anon_user ) + anon_history2 = self.history_manager.create( user=anon_user ) self.trans.set_history( anon_history2 ) - self.assertTrue( self.history_mgr.is_owner( self.trans, anon_history2, anon_user ) ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, anon_history2, anon_user ) ) + self.assertTrue( self.history_manager.is_owner( anon_history2, anon_user, current_history=self.trans.history ) ) + self.assertTrue( self.history_manager.is_accessible( anon_history2, anon_user, current_history=self.trans.history ) ) self.log( "should not allow owner or access for anon user on someone elses history" ) - owner = self.user_mgr.create( self.trans, **user2_data ) - someone_elses = self.history_mgr.create( self.trans, user=owner ) - self.assertFalse( self.history_mgr.is_owner( self.trans, someone_elses, anon_user ) ) - self.assertFalse( self.history_mgr.is_accessible( self.trans, someone_elses, anon_user ) ) + owner = self.user_manager.create( **user2_data ) + someone_elses = self.history_manager.create( user=owner ) + self.assertFalse( self.history_manager.is_owner( someone_elses, anon_user, current_history=self.trans.history ) ) + self.assertFalse( self.history_manager.is_accessible( someone_elses, anon_user, current_history=self.trans.history ) ) self.log( "should allow access for anon user if the history is published or importable" ) - self.history_mgr.make_importable( self.trans, someone_elses ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, someone_elses, anon_user ) ) - self.history_mgr.publish( self.trans, someone_elses ) - self.assertTrue( self.history_mgr.is_accessible( self.trans, someone_elses, anon_user ) ) + self.history_manager.make_importable( someone_elses ) + self.assertTrue( self.history_manager.is_accessible( someone_elses, anon_user, current_history=self.trans.history ) ) + self.history_manager.publish( someone_elses ) + self.assertTrue( self.history_manager.is_accessible( someone_elses, anon_user, current_history=self.trans.history ) ) def test_delete_and_purge( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) + user2 = self.user_manager.create( **user2_data ) self.trans.set_user( user2 ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) + history1 = self.history_manager.create( name='history1', user=user2 ) self.trans.set_history( history1 ) + self.log( "should allow deletion and undeletion" ) self.assertFalse( history1.deleted ) - self.history_mgr.delete( self.trans, history1 ) + self.history_manager.delete( history1 ) self.assertTrue( history1.deleted ) - self.history_mgr.undelete( self.trans, history1 ) + self.history_manager.undelete( history1 ) self.assertFalse( history1.deleted ) - history2 = self.history_mgr.create( self.trans, name='history2', user=user2 ) - self.history_mgr.purge( self.trans, history1 ) - self.assertTrue( history1.purged ) + self.log( "should allow purging" ) + history2 = self.history_manager.create( name='history2', user=user2 ) + self.history_manager.purge( history2 ) + self.assertTrue( history2.purged ) - def test_histories( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) + def test_current( self ): + user2 = self.user_manager.create( **user2_data ) self.trans.set_user( user2 ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) + history1 = self.history_manager.create( name='history1', user=user2 ) self.trans.set_history( history1 ) - history2 = self.history_mgr.create( self.trans, name='history2', user=user2 ) + history2 = self.history_manager.create( name='history2', user=user2 ) self.log( "should be able to set or get the current history for a user" ) - self.assertEqual( self.history_mgr.get_current( self.trans ), history1 ) - self.assertEqual( self.history_mgr.set_current( self.trans, history2 ), history2 ) - self.assertEqual( self.history_mgr.get_current( self.trans ), history2 ) - self.assertEqual( self.history_mgr.set_current_by_id( self.trans, history1.id ), history1 ) - self.assertEqual( self.history_mgr.get_current( self.trans ), history1 ) + self.assertEqual( self.history_manager.get_current( self.trans ), history1 ) + self.assertEqual( self.history_manager.set_current( self.trans, history2 ), history2 ) + self.assertEqual( self.history_manager.get_current( self.trans ), history2 ) + self.assertEqual( self.history_manager.set_current_by_id( self.trans, history1.id ), history1 ) + self.assertEqual( self.history_manager.get_current( self.trans ), history1 ) # ============================================================================= @@ -315,101 +318,104 @@ testable_url_for = lambda *a, **k: '(fake url): %s, %s' % ( a, k ) HistorySerializer.url_for = staticmethod( testable_url_for ) hdas.HDASerializer.url_for = staticmethod( testable_url_for ) - class HistorySerializerTestCase( BaseTestCase ): def set_up_managers( self ): super( HistorySerializerTestCase, self ).set_up_managers() - self.history_mgr = HistoryManager( self.app ) - self.hda_mgr = hdas.HDAManager( self.app ) + self.history_manager = HistoryManager( self.app ) + self.hda_manager = hdas.HDAManager( self.app ) self.history_serializer = HistorySerializer( self.app ) def test_views( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) + user2 = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=user2 ) self.log( 'should have a summary view' ) - summary_view = self.history_serializer.serialize_to_view( self.trans, history1, view='summary' ) + summary_view = self.history_serializer.serialize_to_view( history1, view='summary' ) self.assertKeys( summary_view, self.history_serializer.views[ 'summary' ] ) self.log( 'should have a detailed view' ) - detailed_view = self.history_serializer.serialize_to_view( self.trans, history1, view='detailed' ) + detailed_view = self.history_serializer.serialize_to_view( history1, view='detailed' ) self.assertKeys( detailed_view, self.history_serializer.views[ 'detailed' ] ) self.log( 'should have the summary view as default view' ) - default_view = self.history_serializer.serialize_to_view( self.trans, history1, default_view='summary' ) + default_view = self.history_serializer.serialize_to_view( history1, default_view='summary' ) self.assertKeys( summary_view, self.history_serializer.views[ 'summary' ] ) self.log( 'should have a serializer for all serializable keys' ) - need_no_serializers = ( basestring, bool, type( None ) ) for key in self.history_serializer.serializable_keyset: instantiated_attribute = getattr( history1, key, None ) if not ( ( key in self.history_serializer.serializers ) - or ( isinstance( instantiated_attribute, need_no_serializers ) ) ): + or ( isinstance( instantiated_attribute, self.TYPES_NEEDING_NO_SERIALIZERS ) ) ): self.fail( 'no serializer for: %s (%s)' % ( key, instantiated_attribute ) ) else: self.assertTrue( True, 'all serializable keys have a serializer' ) def test_views_and_keys( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) + user2 = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=user2 ) self.log( 'should be able to use keys with views' ) - serialized = self.history_serializer.serialize_to_view( self.trans, history1, + serialized = self.history_serializer.serialize_to_view( history1, view='summary', keys=[ 'state_ids', 'user_id' ] ) self.assertKeys( serialized, self.history_serializer.views[ 'summary' ] + [ 'state_ids', 'user_id' ] ) self.log( 'should be able to use keys on their own' ) - serialized = self.history_serializer.serialize_to_view( self.trans, history1, + serialized = self.history_serializer.serialize_to_view( history1, keys=[ 'state_ids', 'user_id' ] ) self.assertKeys( serialized, [ 'state_ids', 'user_id' ] ) def test_sharable( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) + user2 = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=user2 ) self.log( 'should have a serializer for all SharableModel keys' ) sharable_attrs = [ 'user_id', 'username_and_slug', 'importable', 'published', 'slug' ] - serialized = self.history_serializer.serialize( self.trans, history1, sharable_attrs ) + serialized = self.history_serializer.serialize( history1, sharable_attrs ) self.assertKeys( serialized, sharable_attrs ) def test_purgable( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) + user2 = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=user2 ) self.log( 'deleted and purged should be returned in their default states' ) keys = [ 'deleted', 'purged' ] - serialized = self.history_serializer.serialize( self.trans, history1, keys ) + serialized = self.history_serializer.serialize( history1, keys ) self.assertEqual( serialized[ 'deleted' ], False ) self.assertEqual( serialized[ 'purged' ], False ) self.log( 'deleted and purged should return their current state' ) - self.history_mgr.delete( self.trans, history1 ) - serialized = self.history_serializer.serialize( self.trans, history1, keys ) + self.history_manager.delete( history1 ) + serialized = self.history_serializer.serialize( history1, keys ) self.assertEqual( serialized[ 'deleted' ], True ) self.assertEqual( serialized[ 'purged' ], False ) - self.history_mgr.purge( self.trans, history1 ) - serialized = self.history_serializer.serialize( self.trans, history1, keys ) + self.history_manager.purge( history1 ) + serialized = self.history_serializer.serialize( history1, keys ) self.assertEqual( serialized[ 'deleted' ], True ) self.assertEqual( serialized[ 'purged' ], True ) def test_history_serializers( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) - - serialized = self.history_serializer.serialize( self.trans, history1, [ 'size', 'nice_size' ]) + user2 = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=user2 ) + all_keys = list( self.history_serializer.serializable_keyset ) + serialized = self.history_serializer.serialize( history1, all_keys ) + + self.log( 'everything serialized should be of the proper type' ) self.assertIsInstance( serialized[ 'size' ], int ) self.assertIsInstance( serialized[ 'nice_size' ], basestring ) + self.log( 'serialized should jsonify well' ) + self.assertIsJsonifyable( serialized ) + def test_contents( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) + user2 = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=user2 ) self.log( 'a history with no contents should be properly reflected in empty, etc.' ) keys = [ 'empty', 'count', 'state_ids', 'state_details', 'state', 'hdas' ] - serialized = self.history_serializer.serialize( self.trans, history1, keys ) + serialized = self.history_serializer.serialize( history1, keys ) self.assertEqual( serialized[ 'state' ], 'new' ) self.assertEqual( serialized[ 'empty' ], True ) self.assertEqual( serialized[ 'count' ], 0 ) @@ -418,10 +424,10 @@ class HistorySerializerTestCase( BaseTestCase ): self.assertIsInstance( serialized[ 'hdas' ], list ) self.log( 'a history with contents should be properly reflected in empty, etc.' ) - hda1 = self.hda_mgr.create( self.trans, history=history1, hid=1 ) - self.hda_mgr.update( self.trans, hda1, dict( state='ok' ) ) + hda1 = self.hda_manager.create( history=history1, hid=1 ) + self.hda_manager.update( hda1, dict( state='ok' ) ) - serialized = self.history_serializer.serialize( self.trans, history1, keys ) + serialized = self.history_serializer.serialize( history1, keys ) self.assertEqual( serialized[ 'state' ], 'ok' ) self.assertEqual( serialized[ 'empty' ], False ) self.assertEqual( serialized[ 'count' ], 1 ) @@ -430,19 +436,23 @@ class HistorySerializerTestCase( BaseTestCase ): self.assertIsInstance( serialized[ 'hdas' ], list ) self.assertIsInstance( serialized[ 'hdas' ][0], basestring ) - serialized = self.history_serializer.serialize( self.trans, history1, [ 'contents' ] ) + serialized = self.history_serializer.serialize( history1, [ 'contents' ] ) self.assertHasKeys( serialized[ 'contents' ][0], [ 'id', 'name', 'peek', 'create_time' ]) -# ============================================================================= -class HistoryDeserializerTestCase( BaseTestCase ): + self.log( 'serialized should jsonify well' ) + self.assertIsJsonifyable( serialized ) - def set_up_managers( self ): - super( HistoryDeserializerTestCase, self ).set_up_managers() - self.history_mgr = HistoryManager( self.app ) - self.history_deserializer = HistoryDeserializer( self.app ) - def test_base( self ): - pass +# # ============================================================================= +# class HistoryDeserializerTestCase( BaseTestCase ): + +# def set_up_managers( self ): +# super( HistoryDeserializerTestCase, self ).set_up_managers() +# self.history_manager = HistoryManager( self.app ) +# self.history_deserializer = HistoryDeserializer( self.app ) + +# def test_base( self ): +# pass # ============================================================================= @@ -450,7 +460,7 @@ class HistoryFiltersTestCase( BaseTestCase ): def set_up_managers( self ): super( HistoryFiltersTestCase, self ).set_up_managers() - self.history_mgr = HistoryManager( self.app ) + self.history_manager = HistoryManager( self.app ) self.filter_parser = HistoryFilters( self.app ) # ---- functional and orm filter splitting and resolution @@ -464,7 +474,7 @@ class HistoryFiltersTestCase( BaseTestCase ): self.assertEqual( len( filters ), 3 ) self.log( 'values should be parsed' ) - self.assertEqual( filters[1].right.value, True ) + self.assertIsInstance( filters[1].right, sqlalchemy.sql.elements.True_ ) def test_parse_filters_invalid_filters( self ): self.log( 'should error on non-column attr') @@ -489,47 +499,47 @@ class HistoryFiltersTestCase( BaseTestCase ): ]) def test_orm_filter_parsing( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) - history2 = self.history_mgr.create( self.trans, name='history2', user=user2 ) - history3 = self.history_mgr.create( self.trans, name='history3', user=user2 ) + user2 = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=user2 ) + history2 = self.history_manager.create( name='history2', user=user2 ) + history3 = self.history_manager.create( name='history3', user=user2 ) filters = self.filter_parser.parse_filters([ ( 'name', 'like', 'history%' ), ]) - histories = self.history_mgr.list( self.trans, filters=filters ) + histories = self.history_manager.list( filters=filters ) #for h in histories: # print h.name self.assertEqual( histories, [ history1, history2, history3 ]) filters = self.filter_parser.parse_filters([ ( 'name', 'like', '%2' ), ]) - self.assertEqual( self.history_mgr.list( self.trans, filters=filters ), [ history2 ]) + self.assertEqual( self.history_manager.list( filters=filters ), [ history2 ]) filters = self.filter_parser.parse_filters([ ( 'name', 'eq', 'history2' ), ]) - self.assertEqual( self.history_mgr.list( self.trans, filters=filters ), [ history2 ]) + self.assertEqual( self.history_manager.list( filters=filters ), [ history2 ]) - self.history_mgr.update( self.trans, history1, dict( deleted=True ) ) + self.history_manager.update( history1, dict( deleted=True ) ) filters = self.filter_parser.parse_filters([ ( 'deleted', 'eq', 'True' ), ]) - self.assertEqual( self.history_mgr.list( self.trans, filters=filters ), [ history1 ]) + self.assertEqual( self.history_manager.list( filters=filters ), [ history1 ]) filters = self.filter_parser.parse_filters([ ( 'deleted', 'eq', 'False' ), ]) - self.assertEqual( self.history_mgr.list( self.trans, filters=filters ), [ history2, history3 ]) - self.assertEqual( self.history_mgr.list( self.trans ), [ history1, history2, history3 ]) + self.assertEqual( self.history_manager.list( filters=filters ), [ history2, history3 ]) + self.assertEqual( self.history_manager.list(), [ history1, history2, history3 ]) - self.history_mgr.update( self.trans, history3, dict( deleted=True ) ) - self.history_mgr.update( self.trans, history1, dict( importable=True ) ) - self.history_mgr.update( self.trans, history2, dict( importable=True ) ) + self.history_manager.update( history3, dict( deleted=True ) ) + self.history_manager.update( history1, dict( importable=True ) ) + self.history_manager.update( history2, dict( importable=True ) ) filters = self.filter_parser.parse_filters([ ( 'deleted', 'eq', 'True' ), ( 'importable', 'eq', 'True' ), ]) - self.assertEqual( self.history_mgr.list( self.trans, filters=filters ), [ history1 ]) - self.assertEqual( self.history_mgr.list( self.trans ), [ history1, history2, history3 ]) + self.assertEqual( self.history_manager.list( filters=filters ), [ history1 ]) + self.assertEqual( self.history_manager.list(), [ history1, history2, history3 ]) def test_fn_filter_parsing( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) - history2 = self.history_mgr.create( self.trans, name='history2', user=user2 ) - history3 = self.history_mgr.create( self.trans, name='history3', user=user2 ) + user2 = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=user2 ) + history2 = self.history_manager.create( name='history2', user=user2 ) + history3 = self.history_manager.create( name='history3', user=user2 ) filters = self.filter_parser.parse_filters([ ( 'annotation', 'has', 'no play' ), ]) anno_filter = filters[0] @@ -540,15 +550,15 @@ class HistoryFiltersTestCase( BaseTestCase ): self.assertTrue( anno_filter( history3 ) ) self.assertFalse( anno_filter( history2 ) ) - self.assertEqual( self.history_mgr.list( self.trans, filters=filters ), [ history3 ]) + self.assertEqual( self.history_manager.list( filters=filters ), [ history3 ]) self.log( 'should allow combinations of orm and fn filters' ) - self.history_mgr.update( self.trans, history3, dict( importable=True ) ) - self.history_mgr.update( self.trans, history2, dict( importable=True ) ) + self.history_manager.update( history3, dict( importable=True ) ) + self.history_manager.update( history2, dict( importable=True ) ) history1.add_item_annotation( self.trans.sa_session, user2, history1, "All work and no play" ) self.trans.sa_session.flush() - shining_examples = self.history_mgr.list( self.trans, filters=self.filter_parser.parse_filters([ + shining_examples = self.history_manager.list( filters=self.filter_parser.parse_filters([ ( 'importable', 'eq', 'True' ), ( 'annotation', 'has', 'no play' ), ])) @@ -579,15 +589,15 @@ class HistoryFiltersTestCase( BaseTestCase ): """ Test limit and offset in conjunction with both orm and fn filtering. """ - user2 = self.user_mgr.create( self.trans, **user2_data ) - history1 = self.history_mgr.create( self.trans, name='history1', user=user2 ) - history2 = self.history_mgr.create( self.trans, name='history2', user=user2 ) - history3 = self.history_mgr.create( self.trans, name='history3', user=user2 ) - history4 = self.history_mgr.create( self.trans, name='history4', user=user2 ) + user2 = self.user_manager.create( **user2_data ) + history1 = self.history_manager.create( name='history1', user=user2 ) + history2 = self.history_manager.create( name='history2', user=user2 ) + history3 = self.history_manager.create( name='history3', user=user2 ) + history4 = self.history_manager.create( name='history4', user=user2 ) - self.history_mgr.delete( self.trans, history1 ) - self.history_mgr.delete( self.trans, history2 ) - self.history_mgr.delete( self.trans, history3 ) + self.history_manager.delete( history1 ) + self.history_manager.delete( history2 ) + self.history_manager.delete( history3 ) test_annotation = "testing" history2.add_item_annotation( self.trans.sa_session, user2, history2, test_annotation ) @@ -601,50 +611,50 @@ class HistoryFiltersTestCase( BaseTestCase ): deleted_and_annotated = [ history2, history3 ] self.log( "no offset, no limit should work" ) - self.assertEqual( self.history_mgr.list( self.trans, offset=None, limit=None ), all_histories ) - self.assertEqual( self.history_mgr.list( self.trans ), all_histories ) + self.assertEqual( self.history_manager.list( offset=None, limit=None ), all_histories ) + self.assertEqual( self.history_manager.list(), all_histories ) self.log( "no offset, limit should work" ) - self.assertEqual( self.history_mgr.list( self.trans, limit=2 ), [ history1, history2 ] ) + self.assertEqual( self.history_manager.list( limit=2 ), [ history1, history2 ] ) self.log( "offset, no limit should work" ) - self.assertEqual( self.history_mgr.list( self.trans, offset=1 ), [ history2, history3, history4 ] ) + self.assertEqual( self.history_manager.list( offset=1 ), [ history2, history3, history4 ] ) self.log( "offset, limit should work" ) - self.assertEqual( self.history_mgr.list( self.trans, offset=1, limit=1 ), [ history2 ] ) + self.assertEqual( self.history_manager.list( offset=1, limit=1 ), [ history2 ] ) self.log( "zero limit should return empty list" ) - self.assertEqual( self.history_mgr.list( self.trans, limit=0 ), [] ) + self.assertEqual( self.history_manager.list( limit=0 ), [] ) self.log( "past len offset should return empty list" ) - self.assertEqual( self.history_mgr.list( self.trans, offset=len( all_histories ) ), [] ) + self.assertEqual( self.history_manager.list( offset=len( all_histories ) ), [] ) self.log( "negative limit should return full list" ) - self.assertEqual( self.history_mgr.list( self.trans, limit=-1 ), all_histories ) + self.assertEqual( self.history_manager.list( limit=-1 ), all_histories ) self.log( "negative offset should return full list" ) - self.assertEqual( self.history_mgr.list( self.trans, offset=-1 ), all_histories ) + self.assertEqual( self.history_manager.list( offset=-1 ), all_histories ) filters = [ model.History.deleted == True ] self.log( "orm filtered, no offset, no limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters ) + found = self.history_manager.list( filters=filters ) self.assertEqual( found, [ history1, history2, history3 ] ) self.log( "orm filtered, no offset, limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters, limit=2 ) + found = self.history_manager.list( filters=filters, limit=2 ) self.assertEqual( found, [ history1, history2 ] ) self.log( "orm filtered, offset, no limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters, offset=1 ) + found = self.history_manager.list( filters=filters, offset=1 ) self.assertEqual( found, [ history2, history3 ] ) self.log( "orm filtered, offset, limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters, offset=1, limit=1 ) + found = self.history_manager.list( filters=filters, offset=1, limit=1 ) self.assertEqual( found, [ history2 ] ) filters = self.filter_parser.parse_filters([ ( 'annotation', 'has', test_annotation ) ]) self.log( "fn filtered, no offset, no limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters ) + found = self.history_manager.list( filters=filters ) self.assertEqual( found, [ history2, history3, history4 ] ) self.log( "fn filtered, no offset, limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters, limit=2 ) + found = self.history_manager.list( filters=filters, limit=2 ) self.assertEqual( found, [ history2, history3 ] ) self.log( "fn filtered, offset, no limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters, offset=1 ) + found = self.history_manager.list( filters=filters, offset=1 ) self.assertEqual( found, [ history3, history4 ] ) self.log( "fn filtered, offset, limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters, offset=1, limit=1 ) + found = self.history_manager.list( filters=filters, offset=1, limit=1 ) self.assertEqual( found, [ history3 ] ) filters = self.filter_parser.parse_filters([ @@ -652,29 +662,29 @@ class HistoryFiltersTestCase( BaseTestCase ): ( 'annotation', 'has', test_annotation ) ]) self.log( "orm and fn filtered, no offset, no limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters ) + found = self.history_manager.list( filters=filters ) self.assertEqual( found, [ history2, history3 ] ) self.log( "orm and fn filtered, no offset, limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters, limit=1 ) + found = self.history_manager.list( filters=filters, limit=1 ) self.assertEqual( found, [ history2 ] ) self.log( "orm and fn filtered, offset, no limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters, offset=1 ) + found = self.history_manager.list( filters=filters, offset=1 ) self.assertEqual( found, [ history3 ] ) self.log( "orm and fn filtered, offset, limit should work" ) - found = self.history_mgr.list( self.trans, filters=filters, offset=1, limit=1 ) + found = self.history_manager.list( filters=filters, offset=1, limit=1 ) self.assertEqual( found, [ history3 ] ) self.log( "orm and fn filtered, zero limit should return empty list" ) - found = self.history_mgr.list( self.trans, filters=filters, limit=0 ) + found = self.history_manager.list( filters=filters, limit=0 ) self.assertEqual( found, [] ) self.log( "orm and fn filtered, past len offset should return empty list" ) - found = self.history_mgr.list( self.trans, filters=filters, offset=len( deleted_and_annotated ) ) + found = self.history_manager.list( filters=filters, offset=len( deleted_and_annotated ) ) self.assertEqual( found, [] ) self.log( "orm and fn filtered, negative limit should return full list" ) - found = self.history_mgr.list( self.trans, filters=filters, limit=-1 ) + found = self.history_manager.list( filters=filters, limit=-1 ) self.assertEqual( found, deleted_and_annotated ) self.log( "orm and fn filtered, negative offset should return full list" ) - found = self.history_mgr.list( self.trans, filters=filters, offset=-1 ) + found = self.history_manager.list( filters=filters, offset=-1 ) self.assertEqual( found, deleted_and_annotated ) diff --git a/test/unit/managers/test_ModelManager.py b/test/unit/managers/test_ModelManager.py index e53adc0efaf..fbaf0eb68af 100644 --- a/test/unit/managers/test_ModelManager.py +++ b/test/unit/managers/test_ModelManager.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ """ from __future__ import print_function @@ -7,6 +6,7 @@ import sys import os import pprint import unittest +import json __GALAXY_ROOT__ = os.getcwd() + '/../../../' sys.path.insert( 1, __GALAXY_ROOT__ + 'lib' ) @@ -61,11 +61,10 @@ class BaseTestCase( unittest.TestCase ): self.app = self.trans.app def set_up_managers( self ): - self.user_mgr = UserManager( self.app ) + self.user_manager = UserManager( self.app ) def set_up_trans( self ): - self.admin_user = self.user_mgr.create( self.trans, - email=admin_email, username='admin', password=default_password ) + self.admin_user = self.user_manager.create( email=admin_email, username='admin', password=default_password ) self.trans.set_user( self.admin_user ) self.trans.set_history( None ) @@ -76,6 +75,8 @@ class BaseTestCase( unittest.TestCase ): print( *args, **kwargs ) # ---- additional test types + TYPES_NEEDING_NO_SERIALIZERS = ( basestring, bool, type( None ), int, float ) + def assertKeys( self, obj, key_list ): self.assertEqual( sorted( obj.keys() ), sorted( key_list ) ) @@ -86,6 +87,51 @@ class BaseTestCase( unittest.TestCase ): else: self.assertTrue( True, 'keys found in object' ) + def assertNullableBasestring( self, item ): + if not isinstance( item, ( basestring, type( None ) ) ): + self.fail( 'Non-nullable basestring: ' + str( type( item ) ) ) + # TODO: len mod 8 and hex re + self.assertTrue( True, 'is nullable basestring: ' + str( item ) ) + + def assertEncodedId( self, item ): + if not isinstance( item, basestring ): + self.fail( 'Non-string: ' + str( type( item ) ) ) + # TODO: len mod 8 and hex re + self.assertTrue( True, 'is id: ' + item ) + + def assertNullableEncodedId( self, item ): + if item is None: + self.assertTrue( True, 'nullable id is None' ) + else: + self.assertEncodedId( item ) + + def assertDate( self, item ): + if not isinstance( item, basestring ): + self.fail( 'Non-string: ' + str( type( item ) ) ) + # TODO: no great way to parse this fully (w/o python-dateutil) + # TODO: re? + self.assertTrue( True, 'is date: ' + item ) + + def assertUUID( self, item ): + if not isinstance( item, basestring ): + self.fail( 'Non-string: ' + str( type( item ) ) ) + # TODO: re for d4d76d69-80d4-4ed7-80c7-211ebcc1a358 + self.assertTrue( True, 'is uuid: ' + item ) + + def assertORMFilter( self, item, msg=None ): + if not isinstance( item, sqlalchemy.sql.elements.BinaryExpression ): + self.fail( 'Not an orm filter: ' + str( type( item ) ) ) + self.assertTrue( True, msg or ( 'is an orm filter: ' + str( item ) ) ) + + def assertFnFilter( self, item, msg=None ): + if not item or not callable( item ): + self.fail( 'Not a fn filter: ' + str( type( item ) ) ) + self.assertTrue( True, msg or ( 'is a fn filter: ' + str( item ) ) ) + + def assertIsJsonifyable( self, item ): + # TODO: use galaxy's override + self.assertIsInstance( json.dumps( item ), basestring ) + # ============================================================================= if __name__ == '__main__': diff --git a/test/unit/managers/test_UserManager.py b/test/unit/managers/test_UserManager.py index 5b78fae5bdf..843f4f0630c 100644 --- a/test/unit/managers/test_UserManager.py +++ b/test/unit/managers/test_UserManager.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +# -*- coding: utf-8 -*- """ """ import sys @@ -19,14 +19,15 @@ from galaxy.util.bunch import Bunch import mock from test_ModelManager import BaseTestCase +from galaxy.managers import users +from galaxy.managers import histories # ============================================================================= -admin_email = 'admin@admin.admin' -admin_users = admin_email default_password = '123456' user2_data = dict( email='user2@user2.user2', username='user2', password=default_password ) user3_data = dict( email='user3@user3.user3', username='user3', password=default_password ) +user4_data = dict( email='user4@user4.user4', username='user4', password=default_password ) # ============================================================================= @@ -38,96 +39,210 @@ class UserManagerTestCase( BaseTestCase ): def test_base( self ): self.log( "should be able to create a user" ) - user2 = self.user_mgr.create( self.trans, **user2_data ) + user2 = self.user_manager.create( **user2_data ) self.assertIsInstance( user2, model.User ) self.assertIsNotNone( user2.id ) self.assertEqual( user2.email, user2_data[ 'email' ] ) self.assertEqual( user2.password, default_password ) - user3 = self.user_mgr.create( self.trans, **user3_data ) + user3 = self.user_manager.create( **user3_data ) self.log( "should be able to query" ) users = self.trans.sa_session.query( model.User ).all() - self.assertEqual( self.user_mgr.list( self.trans ), users ) + self.assertEqual( self.user_manager.list(), users ) - self.assertEqual( self.user_mgr.by_id( self.trans, user2.id ), user2 ) - self.assertEqual( self.user_mgr.by_ids( self.trans, [ user3.id, user2.id ] ), [ user3, user2 ] ) + self.assertEqual( self.user_manager.by_id( user2.id ), user2 ) + self.assertEqual( self.user_manager.by_ids( [ user3.id, user2.id ] ), [ user3, user2 ] ) self.log( "should be able to limit and offset" ) - self.assertEqual( self.user_mgr.list( self.trans, limit=1 ), users[0:1] ) - self.assertEqual( self.user_mgr.list( self.trans, offset=1 ), users[1:] ) - self.assertEqual( self.user_mgr.list( self.trans, limit=1, offset=1 ), users[1:2] ) + self.assertEqual( self.user_manager.list( limit=1 ), users[0:1] ) + self.assertEqual( self.user_manager.list( offset=1 ), users[1:] ) + self.assertEqual( self.user_manager.list( limit=1, offset=1 ), users[1:2] ) - self.assertEqual( self.user_mgr.list( self.trans, limit=0 ), [] ) - self.assertEqual( self.user_mgr.list( self.trans, offset=3 ), [] ) + self.assertEqual( self.user_manager.list( limit=0 ), [] ) + self.assertEqual( self.user_manager.list( offset=3 ), [] ) self.log( "should be able to order" ) - self.assertEqual( self.user_mgr.list( self.trans, order_by=( sqlalchemy.desc( model.User.create_time ) ) ), + self.assertEqual( self.user_manager.list( order_by=( sqlalchemy.desc( model.User.create_time ) ) ), [ user3, user2, self.admin_user ] ) def test_invalid_create( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) + user2 = self.user_manager.create( **user2_data ) self.log( "emails must be unique" ) - self.assertRaises( exceptions.Conflict, self.user_mgr.create, - self.trans, **dict( email='user2@user2.user2', username='user2a', password=default_password ) ) + self.assertRaises( exceptions.Conflict, self.user_manager.create, + **dict( email='user2@user2.user2', username='user2a', password=default_password ) ) self.log( "usernames must be unique" ) - self.assertRaises( exceptions.Conflict, self.user_mgr.create, - self.trans, **dict( email='user2a@user2.user2', username='user2', password=default_password ) ) + self.assertRaises( exceptions.Conflict, self.user_manager.create, + **dict( email='user2a@user2.user2', username='user2', password=default_password ) ) def test_email_queries( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - user3 = self.user_mgr.create( self.trans, **user3_data ) + user2 = self.user_manager.create( **user2_data ) + user3 = self.user_manager.create( **user3_data ) self.log( "should be able to query by email" ) - self.assertEqual( self.user_mgr.by_email( self.trans, user2_data[ 'email' ] ), user2 ) + self.assertEqual( self.user_manager.by_email( user2_data[ 'email' ] ), user2 ) #note: sorted by email alpha users = self.trans.sa_session.query( model.User ).all() - self.assertEqual( self.user_mgr.by_email_like( self.trans, '%@%' ), [ self.admin_user, user2, user3 ] ) + self.assertEqual( self.user_manager.by_email_like( '%@%' ), [ self.admin_user, user2, user3 ] ) def test_admin( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - user3 = self.user_mgr.create( self.trans, **user3_data ) + user2 = self.user_manager.create( **user2_data ) + user3 = self.user_manager.create( **user3_data ) self.log( "should be able to test whether admin" ) - self.assertTrue( self.user_mgr.is_admin( self.trans, self.admin_user ) ) - self.assertFalse( self.user_mgr.is_admin( self.trans, user2 ) ) - self.assertEqual( self.user_mgr.admins( self.trans ), [ self.admin_user ] ) - self.assertRaises( exceptions.AdminRequiredException, self.user_mgr.error_unless_admin, self.trans, user2 ) - self.assertEqual( self.user_mgr.error_unless_admin( self.trans, self.admin_user ), self.admin_user ) + self.assertTrue( self.user_manager.is_admin( self.admin_user ) ) + self.assertFalse( self.user_manager.is_admin( user2 ) ) + self.assertEqual( self.user_manager.admins(), [ self.admin_user ] ) + self.assertRaises( exceptions.AdminRequiredException, self.user_manager.error_unless_admin, user2 ) + self.assertEqual( self.user_manager.error_unless_admin( self.admin_user ), self.admin_user ) def test_anonymous( self ): anon = None - user2 = self.user_mgr.create( self.trans, **user2_data ) + user2 = self.user_manager.create( **user2_data ) self.log( "should be able to tell if a user is anonymous" ) - self.assertRaises( exceptions.AuthenticationFailed, self.user_mgr.error_if_anonymous, self.trans, anon ) - self.assertEqual( self.user_mgr.error_if_anonymous( self.trans, user2 ), user2 ) + self.assertRaises( exceptions.AuthenticationFailed, self.user_manager.error_if_anonymous, anon ) + self.assertEqual( self.user_manager.error_if_anonymous( user2 ), user2 ) def test_current( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - user3 = self.user_mgr.create( self.trans, **user3_data ) + user2 = self.user_manager.create( **user2_data ) + user3 = self.user_manager.create( **user3_data ) self.log( "should be able to tell if a user is the current (trans) user" ) - self.assertEqual( self.user_mgr.current_user( self.trans ), self.admin_user ) - self.assertNotEqual( self.user_mgr.current_user( self.trans ), user2 ) + self.assertEqual( self.user_manager.current_user( self.trans ), self.admin_user ) + self.assertNotEqual( self.user_manager.current_user( self.trans ), user2 ) def test_api_keys( self ): - user2 = self.user_mgr.create( self.trans, **user2_data ) - user3 = self.user_mgr.create( self.trans, **user3_data ) + user2 = self.user_manager.create( **user2_data ) + user3 = self.user_manager.create( **user3_data ) self.log( "should return None if no APIKey has been created" ) - self.assertEqual( self.user_mgr.valid_api_key( self.trans, user2 ), None ) + self.assertEqual( self.user_manager.valid_api_key( user2 ), None ) self.log( "should be able to generate and retrieve valid api key" ) - user2_api_key = self.user_mgr.create_api_key( self.trans, user2 ) + user2_api_key = self.user_manager.create_api_key( user2 ) self.assertIsInstance( user2_api_key, basestring ) - self.assertEqual( self.user_mgr.valid_api_key( self.trans, user2 ).key, user2_api_key ) + self.assertEqual( self.user_manager.valid_api_key( user2 ).key, user2_api_key ) self.log( "should return the most recent (i.e. most valid) api key" ) - user2_api_key_2 = self.user_mgr.create_api_key( self.trans, user2 ) - self.assertEqual( self.user_mgr.valid_api_key( self.trans, user2 ).key, user2_api_key_2 ) + user2_api_key_2 = self.user_manager.create_api_key( user2 ) + self.assertEqual( self.user_manager.valid_api_key( user2 ).key, user2_api_key_2 ) + + +# ============================================================================= +class UserSerializerTestCase( BaseTestCase ): + + def set_up_managers( self ): + super( UserSerializerTestCase, self ).set_up_managers() + self.user_serializer = users.UserSerializer( self.app ) + + def test_views( self ): + user = self.user_manager.create( **user2_data ) + + self.log( 'should have a summary view' ) + summary_view = self.user_serializer.serialize_to_view( user, view='summary' ) + self.assertKeys( summary_view, self.user_serializer.views[ 'summary' ] ) + + self.log( 'should have the summary view as default view' ) + default_view = self.user_serializer.serialize_to_view( user, default_view='summary' ) + self.assertKeys( summary_view, self.user_serializer.views[ 'summary' ] ) + + self.log( 'should have a serializer for all serializable keys' ) + for key in self.user_serializer.serializable_keyset: + instantiated_attribute = getattr( user, key, None ) + if not ( ( key in self.user_serializer.serializers ) + or ( isinstance( instantiated_attribute, self.TYPES_NEEDING_NO_SERIALIZERS ) ) ): + self.fail( 'no serializer for: %s (%s)' % ( key, instantiated_attribute ) ) + else: + self.assertTrue( True, 'all serializable keys have a serializer' ) + + def test_views_and_keys( self ): + user = self.user_manager.create( **user2_data ) + + self.log( 'should be able to use keys with views' ) + serialized = self.user_serializer.serialize_to_view( user, + view='summary', keys=[ 'create_time' ] ) + self.assertKeys( serialized, + self.user_serializer.views[ 'summary' ] + [ 'create_time' ] ) + + self.log( 'should be able to use keys on their own' ) + serialized = self.user_serializer.serialize_to_view( user, + keys=[ 'tags_used', 'is_admin' ] ) + self.assertKeys( serialized, [ 'tags_used', 'is_admin' ] ) + + def test_serializers( self ): + user = self.user_manager.create( **user2_data ) + all_keys = list( self.user_serializer.serializable_keyset ) + serialized = self.user_serializer.serialize( user, all_keys, trans=self.trans ) + # pprint.pprint( serialized ) + + self.log( 'everything serialized should be of the proper type' ) + self.assertEncodedId( serialized[ 'id' ] ) + self.assertDate( serialized[ 'create_time' ] ) + self.assertDate( serialized[ 'update_time' ] ) + self.assertIsInstance( serialized[ 'deleted' ], bool ) + self.assertIsInstance( serialized[ 'purged' ], bool ) + + # self.assertIsInstance( serialized[ 'active' ], bool ) + self.assertIsInstance( serialized[ 'is_admin' ], bool ) + self.assertIsInstance( serialized[ 'total_disk_usage' ], float ) + self.assertIsInstance( serialized[ 'nice_total_disk_usage' ], basestring ) + self.assertIsInstance( serialized[ 'quota_percent' ], ( type( None ), float ) ) + self.assertIsInstance( serialized[ 'tags_used' ], list ) + self.assertIsInstance( serialized[ 'requests' ], list ) + + self.log( 'serialized should jsonify well' ) + self.assertIsJsonifyable( serialized ) + + +class CurrentUserSerializerTestCase( BaseTestCase ): + + def set_up_managers( self ): + super( CurrentUserSerializerTestCase, self ).set_up_managers() + self.history_manager = histories.HistoryManager( self.app ) + self.user_serializer = users.CurrentUserSerializer( self.app ) + + def test_anonymous( self ): + anonym = None + # need a history here for total_disk_usage + self.trans.set_history( self.history_manager.create() ) + + self.log( 'should be able to serialize anonymous user' ) + serialized = self.user_serializer.serialize_to_view( anonym, view='detailed', trans=self.trans ) + self.assertKeys( serialized, + [ 'id', 'total_disk_usage', 'nice_total_disk_usage', 'quota_percent' ] ) + + self.log( 'anonymous\'s id should be None' ) + self.assertEqual( serialized[ 'id' ], None ) + self.log( 'everything serialized should be of the proper type' ) + self.assertIsInstance( serialized[ 'total_disk_usage' ], float ) + self.assertIsInstance( serialized[ 'nice_total_disk_usage' ], basestring ) + self.assertIsInstance( serialized[ 'quota_percent' ], ( type( None ), float ) ) + + self.log( 'serialized should jsonify well' ) + self.assertIsJsonifyable( serialized ) + + +# ============================================================================= +class AdminUserFilterParserTestCase( BaseTestCase ): + + def set_up_managers( self ): + super( AdminUserFilterParserTestCase, self ).set_up_managers() + self.filter_parser = users.AdminUserFilterParser( self.app ) + + def test_parsable( self ): + self.log( 'the following filters should be parsable' ) + self.assertORMFilter( self.filter_parser.parse_filter( 'email', 'eq', 'wot' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'email', 'contains', 'wot' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'email', 'like', 'wot' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'username', 'eq', 'wot' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'username', 'contains', 'wot' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'username', 'like', 'wot' ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'active', 'eq', True ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'disk_usage', 'le', 500000.00 ) ) + self.assertORMFilter( self.filter_parser.parse_filter( 'disk_usage', 'ge', 500000.00 ) ) # =============================================================================
",e,"",f,"
",b,"",a[0],"",a[1],"