summaryrefslogtreecommitdiff
path: root/application.rb
diff options
context:
space:
mode:
Diffstat (limited to 'application.rb')
-rw-r--r--application.rb532
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