diff options
Diffstat (limited to 'application.rb')
-rw-r--r-- | application.rb | 532 |
1 files changed, 348 insertions, 184 deletions
diff --git a/application.rb b/application.rb index 7a18fd5..1771661 100644 --- a/application.rb +++ b/application.rb @@ -1,252 +1,416 @@ ['rubygems', "haml", "sass", "rack-flash"].each do |lib| - require lib + require lib end -gem "opentox-ruby-api-wrapper", "= 1.6.5" -require 'opentox-ruby-api-wrapper' +gem "opentox-ruby", "~> 1" +require 'opentox-ruby' gem 'sinatra-static-assets' require 'sinatra/static_assets' require 'ftools' require File.join(File.dirname(__FILE__),'model.rb') require File.join(File.dirname(__FILE__),'helper.rb') -require File.join(File.dirname(__FILE__),'parser.rb') +use Rack::Session::Cookie, :expire_after => 28800, + :secret => "ui6vaiNi-change_me" use Rack::Flash -set :sessions, true + +set :lock, true + +helpers do + + def error(message) + LOGGER.error message + @model.update :status => "Error", :error_messages => message + flash[:notice] = message + redirect url_for('/create') + end + + private + def delete_model(model, subjectid=nil) + task = OpenTox::Task.create("Deleting model: #{model.uri}",url_for("/delete",:full)) do |task| + begin RestClient.put(File.join(model.task_uri, 'Cancelled'),subjectid) if model.task_uri rescue LOGGER.warn "Cannot cancel task #{model.task_uri}" end + task.progress(15) + delete_dependent(model.uri, subjectid) if model.uri + task.progress(30) + delete_dependent(model.validation_uri, subjectid) if model.validation_uri + task.progress(45) + delete_dependent(model.validation_report_uri, subjectid) if model.validation_report_uri + task.progress(60) + delete_dependent(model.validation_qmrf_uri, subjectid) if model.validation_qmrf_uri + task.progress(75) + delete_dependent(model.training_dataset, subjectid) if model.training_dataset + task.progress(90) + delete_dependent(model.feature_dataset, subjectid) if model.feature_dataset + task.progress(100) + "" + end + end + + def delete_dependent(uri, subjectid=nil) + begin + RestClient.delete(uri, :subjectid => subjectid) if subjectid + RestClient.delete(uri) if !subjectid + rescue + LOGGER.warn "Can not delete uri: #{uri}" + end + end +end + +before do + if !logged_in and !( env['REQUEST_URI'] =~ /\/login$/ and env['REQUEST_METHOD'] == "POST" ) #or !AA_SERVER + login("guest","guest") + end +end get '/?' do - redirect url_for('/create') + redirect url_for('/create') end -get '/models/?' do - @models = ToxCreateModel.all(:order => [ :created_at.desc ]) - @models.each { |model| model.process } - haml :models +get '/login' do + haml :login end -delete '/model/:id/?' do - model = ToxCreateModel.get(params[:id]) - begin - RestClient.delete model.uri if model.uri - RestClient.delete model.task_uri if model.task_uri - model.destroy! - flash[:notice] = "#{model.name} model deleted." - rescue - flash[:notice] = "#{model.name} model delete error." - end - redirect url_for('/models') +get '/models/?' do + @models = ToxCreateModel.all.reverse + subjectstring = session[:subjectid] ? "?subjectid=#{CGI.escape(session[:subjectid])}" : "" + haml :models, :locals=>{:models=>@models, :subjectstring => subjectstring} end get '/model/:id/status/?' do response['Content-Type'] = 'text/plain' - model = ToxCreateModel.get(params[:id]) - begin - haml :model_status, :locals=>{:model=>model}, :layout => false - rescue + model = ToxCreateModel.get(params[:id]) + begin + haml :model_status, :locals=>{:model=>model}, :layout => false + rescue + return "Model #{params[:id]} not available" + end +end + +get '/model/:id/progress/?' do + response['Content-Type'] = 'text/plain' + model = ToxCreateModel.get(params[:id]) + if model.task_uri + if (OpenTox::Task.exist?(model.task_uri)) + task = OpenTox::Task.exist?(model.task_uri) + percentage_completed = task.percentageCompleted + end + begin + haml :model_progress, :locals=>{:percentage_completed=>percentage_completed}, :layout => false + rescue + return "unavailable" + end + else + return "" + end +end + +get '/model/:id/name/?' do + response['Content-Type'] = 'text/plain' + model = ToxCreateModel.get(params[:id]) + begin + case params[:mode] + when 'edit' + haml :model_name_edit, :locals=>{:model=>model}, :layout => false + when 'show' + haml :model_name, :locals=>{:model=>model}, :layout => false + else + params.inspect + end + rescue return "unavailable" - end + end end -get '/model/:id/:view/?' do +put '/model/:id/?' do response['Content-Type'] = 'text/plain' - model = ToxCreateModel.get(params[:id]) + model = ToxCreateModel.get(params[:id]) + begin + model.update :name => params[:name] if params[:name] && model.name != params[:name] + redirect url_for("/model/#{model.id}/name?mode=show") + rescue + return "unavailable" + end +end + +get '/model/:id/:view/?' do + response['Content-Type'] = 'text/plain' + model = ToxCreateModel.get(params[:id]) + subjectstring = session[:subjectid] ? "?subjectid=#{CGI.escape(session[:subjectid])}" : "" begin - model.process - model.save case params[:view] when "model" - haml :model, :locals=>{:model=>model}, :layout => false - when /validation/ - haml :validation, :locals=>{:model=>model}, :layout => false - else - return "unable to render model: id #{params[:id]}, view #{params[:view]}" - end - rescue + haml :model, :locals=>{:model=>model,:subjectstring => subjectstring}, :layout => false + when /validation/ + haml :validation, :locals=>{:model=>model,:subjectstring => subjectstring}, :layout => false + else + return "unable to render model: id #{params[:id]}, view #{params[:view]}" + end + rescue return "unable to render model: id #{params[:id]}, view #{params[:view]}" - end + end end get '/predict/?' do - @models = ToxCreateModel.all(:order => [ :created_at.desc ]) - @models = @models.collect{|m| m if m.status == 'Completed'}.compact - haml :predict + @models = ToxCreateModel.all.reverse + @models = @models.collect{|m| m if m.status == 'Completed'}.compact + haml :predict end get '/create' do - haml :create + haml :create end get '/help' do - haml :help + haml :help end get "/confidence" do - haml :confidence + haml :confidence +end + +# proxy to get data from compound service +# (jQuery load does not work with external URIs) +get %r{/compound/(.*)} do |inchi| + inchi = URI.unescape request.env['REQUEST_URI'].sub(/^\//,'').sub(/.*compound\//,'') + OpenTox::Compound.from_inchi(inchi).to_names.join(', ') end -post '/upload' do # create a new model - - if params[:endpoint] == '' - flash[:notice] = "Please enter an endpoint name." - redirect url_for('/create') - end - unless params[:endpoint] and params[:file] and params[:file][:tempfile] - flash[:notice] = "Please enter an endpoint name and upload a Excel or CSV file." - redirect url_for('/create') - end - - @model = ToxCreateModel.new - @model.name = params[:endpoint] - feature_uri = url_for("/feature#"+URI.encode(params[:endpoint]), :full) - parser = Parser.new params[:file], feature_uri - - unless parser.format_errors.empty? - flash[:notice] = "Incorrect file format. Please follow the instructions for #{link_to "Excel", "/excel_format"} or #{link_to "CSV", "/csv_format"} formats." - end - - if parser.dataset.compounds.empty? - flash[:notice] = "Dataset #{params[:file][:filename]} is empty." - redirect url_for('/create') - end - - begin - @model.task_uri = OpenTox::Algorithm::Lazar.create_model(:dataset_uri => parser.dataset_uri, :prediction_feature => feature_uri) - rescue - flash[:notice] = "Model creation failed. Please check if the input file is in a valid #{link_to "Excel", "/excel_format"} or #{link_to "CSV", "/csv_format"} format." - redirect url_for('/create') - end +post '/models' do # create a new model + unless params[:file] and params[:file][:tempfile] #params[:endpoint] and + flash[:notice] = "Please upload a Excel or CSV file." + redirect url_for('/create') + end - begin - validation_task_uri = OpenTox::Validation.crossvalidation( - :algorithm_uri => OpenTox::Algorithm::Lazar.uri, - :dataset_uri => parser.dataset_uri, - :prediction_feature => feature_uri, - :algorithm_params => "feature_generation_uri=#{OpenTox::Algorithm::Fminer.uri}" - ).uri - LOGGER.debug "Validation task: " + validation_task_uri - @model.validation_task_uri = validation_task_uri - rescue - flash[:notice] = "Model validation failed." + unless logged_in() + logout + flash[:notice] = "Please login to create a new model." + redirect url_for('/create') end + subjectid = session[:subjectid] ? session[:subjectid] : nil + @model = ToxCreateModel.create(:name => params[:file][:filename].sub(/\..*$/,""), :subjectid => subjectid) + @model.update :web_uri => url_for("/model/#{@model.id}", :full), :warnings => "" + task = OpenTox::Task.create("Uploading dataset and creating lazar model",url_for("/models",:full)) do |task| -=begin - if parser.nr_compounds < 10 - flash[:notice] = "Too few compounds to create a prediction model. Did you provide compounds in SMILES format and classification activities as described in the #{link_to "instructions", "/excel_format"}? As a rule of thumb you will need at least 100 training compounds for nongeneric datasets. A lower number could be sufficient for congeneric datasets." - redirect url_for('/create') - end -=end + task.progress(5) + @model.update :status => "Uploading and saving dataset" + begin + @dataset = OpenTox::Dataset.create(nil, subjectid) + # check format by extension - not all browsers provide correct content-type]) + case File.extname(params[:file][:filename]) + when ".csv" + csv = params[:file][:tempfile].read + @dataset.load_csv(csv, subjectid) + when ".xls", ".xlsx" + @dataset.load_spreadsheet(Excel.new params[:file][:tempfile].path, subjectid) + else + error "#{params[:file][:filename]} has a unsupported file type." + end + rescue => e + error "Dataset creation failed with #{e.message}" + end + @dataset.save(subjectid) + task.progress(10) + if @dataset.compounds.size < 10 + error "Too few compounds to create a prediction model. Did you provide compounds in SMILES format and classification activities as described in the #{link_to "instructions", "/excel_format"}? As a rule of thumb you will need at least 100 training compounds for nongeneric datasets. A lower number could be sufficient for congeneric datasets." + end + if @dataset.features.keys.size != 1 + error "More than one feature in dataset #{params[:file][:filename]}. Please delete irrelvant columns and try again." + end + if @dataset.metadata[OT.Errors] + error "Incorrect file format. Please follow the instructions for #{link_to "Excel", "/excel_format"} or #{link_to "CSV", "/csv_format"} formats." + end + @model.update :training_dataset => @dataset.uri, :nr_compounds => @dataset.compounds.size, :status => "Creating prediction model" + @model.update :warnings => @dataset.metadata[OT.Warnings] unless @dataset.metadata[OT.Warnings].empty? + task.progress(15) + begin + lazar = OpenTox::Model::Lazar.create(:dataset_uri => @dataset.uri, :subjectid => subjectid) + rescue => e + error "Model creation failed with '#{e.message}'. Please check if the input file is in a valid #{link_to "Excel", "/excel_format"} or #{link_to "CSV", "/csv_format"} format." + end + task.progress(25) + type = "unknown" + case lazar.metadata[OT.isA] + when /Classification/ + type = "classification" + when /Regression/ + type = "regression" + end + @model.update :type => type, :feature_dataset => lazar.metadata[OT.featureDataset], :uri => lazar.uri - @model.nr_compounds = parser.nr_compounds - @model.warnings = '' + unless url_for("",:full).match(/localhost/) + @model.update :status => "Validating model" + begin + validation = OpenTox::Crossvalidation.create( + {:algorithm_uri => lazar.metadata[OT.algorithm], + :dataset_uri => lazar.parameter("dataset_uri"), + :subjectid => subjectid, + :prediction_feature => lazar.parameter("prediction_feature"), + :algorithm_params => "feature_generation_uri=#{lazar.parameter("feature_generation_uri")}"}, + nil, OpenTox::SubTask.new(task,25,80)) + @model.update(:validation_uri => validation.uri) + LOGGER.debug "Validation URI: #{@model.validation_uri}" - @model.warnings += "<p>Incorrect Smiles structures (ignored):</p>" + parser.smiles_errors.join("<br/>") unless parser.smiles_errors.empty? - @model.warnings += "<p>Irregular activities (ignored):</p>" + parser.activity_errors.join("<br/>") unless parser.activity_errors.empty? - duplicate_warnings = '' - parser.duplicates.each {|inchi,lines| duplicate_warnings += "<p>#{lines.join('<br/>')}</p>" if lines.size > 1 } - @model.warnings += "<p>Duplicated structures (all structures/activities used for model building, please make sure, that the results were obtained from <em>independent</em> experiments):</p>" + duplicate_warnings unless duplicate_warnings.empty? - @model.save + # create summary + validation.summary(subjectid).each do |k,v| + #LOGGER.debug "mr ::: k: #{k.inspect} - v: #{v.inspect}" + begin + eval "@model.update :#{k.to_s} => v" if v + rescue + eval "@model.update :#{k.to_s} => 0" + end + end - flash[:notice] = "Model creation and validation started - this may last up to several hours depending on the number and size of the training compounds." - redirect url_for('/models') + @model.update :status => "Creating validation report" + validation_report_uri = validation.find_or_create_report(subjectid, OpenTox::SubTask.new(task,80,90)) #unless @model.dirty? + @model.update :validation_report_uri => validation_report_uri, :status => "Creating QMRF report" + qmrf_report = OpenTox::Crossvalidation::QMRFReport.create(@model.uri, subjectid, OpenTox::SubTask.new(task,90,99)) + @model.update(:validation_qmrf_uri => qmrf_report.uri, :status => "Completed") + + rescue => e + LOGGER.debug "Model validation failed with #{e.message}." + @model.save # to avoid dirty models + @model.update :warnings => @model.warnings + "\nModel validation failed with #{e.message}.", :status => "Error", :error_messages => e.message + end + + end + + + #@model.warnings += "<p>Incorrect Smiles structures (ignored):</p>" + parser.smiles_errors.join("<br/>") unless parser.smiles_errors.empty? + #@model.warnings += "<p>Irregular activities (ignored):</p>" + parser.activity_errors.join("<br/>") unless parser.activity_errors.empty? + #duplicate_warnings = '' + #parser.duplicates.each {|inchi,lines| duplicate_warnings += "<p>#{lines.join('<br/>')}</p>" if lines.size > 1 } + #@model.warnings += "<p>Duplicated structures (all structures/activities used for model building, please make sure, that the results were obtained from <em>independent</em> experiments):</p>" + duplicate_warnings unless duplicate_warnings.empty? + lazar.uri + end + @model.update(:task_uri => task.uri) + + flash[:notice] = "Model creation and validation started - this may last up to several hours depending on the number and size of the training compounds." + redirect url_for('/models') - # TODO: check for empty model end post '/predict/?' do # post chemical name to model - @identifier = params[:identifier] - unless params[:selection] and params[:identifier] != '' - flash[:notice] = "Please enter a compound identifier and select an endpoint from the list." - redirect url_for('/predict') - end - begin - @compound = OpenTox::Compound.new(:name => params[:identifier]) - rescue - flash[:notice] = "Could not find a structure for '#{@identifier}'. Please try again." - redirect url_for('/predict') - end - @predictions = [] - params[:selection].keys.each do |id| - model = ToxCreateModel.get(id.to_i) - model.process unless model.uri - prediction = nil - confidence = nil - title = nil - db_activities = [] - #LOGGER.debug "curl -X POST -d 'compound_uri=#{@compound.uri}' -H 'Accept:application/x-yaml' #{model.uri}" - prediction = YAML.load(`curl -X POST -d 'compound_uri=#{@compound.uri}' -H 'Accept:application/x-yaml' #{model.uri}`) - #prediction = YAML.load(OpenTox::Model::Lazar.predict(params[:compound_uri],params[:model_uri])) - source = prediction.creator - if prediction.data[@compound.uri] - if source.to_s.match(/model/) # real prediction - prediction = prediction.data[@compound.uri].first.values.first - #LOGGER.debug prediction[File.join(@@config[:services]["opentox-model"],"lazar#classification")] - #LOGGER.debug prediction[File.join(@@config[:services]["opentox-model"],"lazar#confidence")] - if !prediction[File.join(@@config[:services]["opentox-model"],"lazar#classification")].nil? - @predictions << { - :title => model.name, - :model_uri => model.uri, - :prediction => prediction[File.join(@@config[:services]["opentox-model"],"lazar#classification")], - :confidence => prediction[File.join(@@config[:services]["opentox-model"],"lazar#confidence")] - } - elsif !prediction[File.join(@@config[:services]["opentox-model"],"lazar#regression")].nil? - @predictions << { - :title => model.name, - :model_uri => model.uri, - :prediction => prediction[File.join(@@config[:services]["opentox-model"],"lazar#regression")], - :confidence => prediction[File.join(@@config[:services]["opentox-model"],"lazar#confidence")] - } - end - else # database value - prediction = prediction.data[@compound.uri].first.values - @predictions << {:title => model.name, :measured_activities => prediction} - end - else - @predictions << {:title => model.name, :prediction => "not available (not enough similar compounds in the training dataset)"} - end - end - LOGGER.debug @predictions.inspect - - haml :prediction + subjectid = session[:subjectid] ? session[:subjectid] : nil + @identifier = params[:identifier] + unless params[:selection] and params[:identifier] != '' + flash[:notice] = "Please enter a compound identifier and select an endpoint from the list." + redirect url_for('/predict') + end + begin + @compound = OpenTox::Compound.from_name(params[:identifier]) + rescue + flash[:notice] = "Could not find a structure for '#{@identifier}'. Please try again." + redirect url_for('/predict') + end + @predictions = [] + params[:selection].keys.each do |id| + model = ToxCreateModel.get(id.to_i) + prediction = nil + confidence = nil + title = nil + db_activities = [] + lazar = OpenTox::Model::Lazar.new model.uri + prediction_dataset_uri = lazar.run({:compound_uri => @compound.uri, :subjectid => subjectid}) + prediction_dataset = OpenTox::LazarPrediction.find(prediction_dataset_uri, subjectid) + if prediction_dataset.metadata[OT.hasSource].match(/dataset/) + @predictions << { + :title => model.name, + :measured_activities => prediction_dataset.measured_activities(@compound) + } + else + predicted_feature = prediction_dataset.metadata[OT.dependentVariables] + prediction = OpenTox::Feature.find(predicted_feature) + if prediction.metadata[OT.error] + @predictions << { + :title => model.name, + :error => prediction.metadata[OT.error] + } + else + @predictions << { + :title => model.name, + :model_uri => model.uri, + :prediction => prediction.metadata[OT.prediction], + :confidence => prediction.metadata[OT.confidence] + } + end + end + # TODO failed/unavailable predictions + end + + haml :prediction end post "/lazar/?" do # get detailed prediction @page = 0 @page = params[:page].to_i if params[:page] @model_uri = params[:model_uri] - @prediction = YAML.load(OpenTox::Model::Lazar.predict(params[:compound_uri],params[:model_uri])) - @compound = OpenTox::Compound.new(:uri => params[:compound_uri]) - @title = @prediction.title - if @prediction.data[@compound.uri] - if @prediction.creator.to_s.match(/model/) # real prediction - p = @prediction.data[@compound.uri].first.values.first - if !p[File.join(@@config[:services]["opentox-model"],"lazar#classification")].nil? - feature = File.join(@@config[:services]["opentox-model"],"lazar#classification") - elsif !p[File.join(@@config[:services]["opentox-model"],"lazar#regression")].nil? - feature = File.join(@@config[:services]["opentox-model"],"lazar#regression") - end - @activity = p[feature] - @confidence = p[File.join(@@config[:services]["opentox-model"],"lazar#confidence")] - @neighbors = p[File.join(@@config[:services]["opentox-model"],"lazar#neighbors")] - @features = p[File.join(@@config[:services]["opentox-model"],"lazar#features")] - else # database value - @measured_activities = @prediction.data[@compound.uri].first.values - end + lazar = OpenTox::Model::Lazar.new @model_uri + prediction_dataset_uri = lazar.run(:compound_uri => params[:compound_uri], :subjectid => params[:subjectid]) + @prediction = OpenTox::LazarPrediction.find(prediction_dataset_uri, session[:subjectid]) + @compound = OpenTox::Compound.new(params[:compound_uri]) + haml :lazar +end + +post '/login' do + if params[:username] == '' || params[:password] == '' + flash[:notice] = "Please enter username and password." + redirect url_for('/login') + end + if login(params[:username], params[:password]) + flash[:notice] = "Welcome #{session[:username]}!" + redirect url_for('/create') else - @activity = "not available (no similar compounds in the training dataset)" + flash[:notice] = "Login failed. Please try again." + haml :login end - haml :lazar end -# proxy to get data from compound service -# (jQuery load does not work with external URIs) -get %r{/compound/(.*)} do |inchi| - OpenTox::Compound.new(:inchi => inchi).names.gsub(/\n/,', ') +post '/logout' do + logout + redirect url_for('/login') +end + +delete '/model/:id/?' do + model = ToxCreateModel.get(params[:id]) + raise OpenTox::NotFoundError.new("Model with id: #{params[:id]} not found!") unless model + begin + delete_model(model, @subjectid) + model.delete + unless ToxCreateModel.get(params[:id]) + begin + aa = OpenTox::Authorization.delete_policies_from_uri(model.web_uri, @subjectid) + LOGGER.debug "Policy deleted for Dataset URI: #{uri} with result: #{aa}" + rescue + LOGGER.warn "Policy delete error for Dataset URI: #{uri}" + end + end + flash[:notice] = "#{model.name} model deleted." + rescue + flash[:notice] = "#{model.name} model delete error." + end + redirect url_for('/models') end delete '/?' do - ToxCreateModel.auto_migrate! - response['Content-Type'] = 'text/plain' - "All Models deleted." + ToxCreateModel.all.each do |model| + begin + delete_model(model, @subjectid) + model.delete + unless ToxCreateModel.get(params[:id]) + begin + aa = OpenTox::Authorization.delete_policies_from_uri(model.web_uri, @subjectid) + LOGGER.debug "Policy deleted for Dataset URI: #{uri} with result: #{aa}" + rescue + LOGGER.warn "Policy delete error for Dataset URI: #{uri}" + end + end + LOGGER.debug "#{model.name} model deleted." + rescue + LOGGER.error "#{model.name} model delete error." + end + end + response['Content-Type'] = 'text/plain' + "All Models deleted." end # SASS stylesheet |