From b3f1f9185d3c46a0b3680812f9a7158628823edf Mon Sep 17 00:00:00 2001 From: leandroradusky Date: Fri, 7 Jul 2023 16:42:07 +0200 Subject: [PATCH] Specific purposes nih tables exporting (#1959) * Specific purposes nih tables exporting * Fixes PR review * Lint fixes * Fixes PR review --- app/controllers/nih_tables_controller.rb | 5 +- app/models/samples_report.rb | 5 ++ .../nih_challenge.csv.csvbuilder | 54 +++++++++++++++++++ .../samples_reports/nih_lod.csv.csvbuilder | 44 +++++++++++++++ app/views/samples_reports/show.haml | 39 +++++++++++--- .../controllers/nih_tables_controller_spec.rb | 47 +++++++++++++++- 6 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 app/views/samples_reports/nih_challenge.csv.csvbuilder create mode 100644 app/views/samples_reports/nih_lod.csv.csvbuilder diff --git a/app/controllers/nih_tables_controller.rb b/app/controllers/nih_tables_controller.rb index b1de0d0e3..53dcddf76 100644 --- a/app/controllers/nih_tables_controller.rb +++ b/app/controllers/nih_tables_controller.rb @@ -1,6 +1,7 @@ class NihTablesController < ApplicationController def show @samples_report = SamplesReport.find(params[:id]) + @target_batch = @samples_report.target_batch return unless authorize_resource(@samples_report, READ_SAMPLES_REPORT) zip_data = create_zip_file @@ -21,9 +22,9 @@ def create_zip_file add_nih_table('results', stream) if purpose == "LOD" - #add_nih_table('lod', stream) + add_nih_table('lod', stream) elsif purpose == "Challenge" - #add_nih_table('challenge', stream) + add_nih_table('challenge', stream) end end zip_stream.rewind diff --git a/app/models/samples_report.rb b/app/models/samples_report.rb index 8db257aa6..0d11f8520 100644 --- a/app/models/samples_report.rb +++ b/app/models/samples_report.rb @@ -68,6 +68,11 @@ def calculate_lod_and_lob self.save end + def target_batch + # The target batch for this box is the batch of any sample which distractor is false or null + samples_report_samples.joins(:sample).find_by("samples.distractor IS NULL OR samples.distractor = 'false'").sample.batch + end + private def there_are_samples diff --git a/app/views/samples_reports/nih_challenge.csv.csvbuilder b/app/views/samples_reports/nih_challenge.csv.csvbuilder new file mode 100644 index 000000000..8abfc22ad --- /dev/null +++ b/app/views/samples_reports/nih_challenge.csv.csvbuilder @@ -0,0 +1,54 @@ +csv << [ + 'protocol_id', + 'technology_platform', + 'vqa_box_id', + 'sample_group', + 'target_analyte_type', + 'target_organism_name', + 'target_organism_taxonomy_id', + 'pango_lineage', + 'who_label', + 'non_clinical_cutoff_min', + 'non_clinical_cutoff_unit', + 'true_positives', + 'false_positives', + 'true_negatives', + 'false_negatives', + 'non_clinical_sensitivity', + 'non_clinical_sensitivity_ci_percent', + 'non_clinical_sensitivity_ci_min', + 'non_clinical_specificity', + 'non_clinical_specificity_ci_percent', + 'non_clinical_specificity_ci_min', + 'area_under_the_roc_curve', + 'roc_x_values', + 'roc_y_values' +] + +sample = @samples_report.samples[0] +csv << [ + nil, + nil, + sample.box.uuid, + "#{sample.box.purpose}-panel", + 'inactivated virus', + 'SARS-CoV-2', + @target_batch.target_organism_taxonomy_id, + @target_batch.pango_lineage, + @target_batch.who_label, + @samples_report.threshold, + 'copies/ml', + params[:true_positives], + params[:false_positives], + params[:true_negatives], + params[:false_negatives], + params[:tpr], + nil, + nil, + params[:fpr], + nil, + nil, + params[:auc], + params[:roc_x_values], + params[:roc_y_values] +] \ No newline at end of file diff --git a/app/views/samples_reports/nih_lod.csv.csvbuilder b/app/views/samples_reports/nih_lod.csv.csvbuilder new file mode 100644 index 000000000..ab093fd00 --- /dev/null +++ b/app/views/samples_reports/nih_lod.csv.csvbuilder @@ -0,0 +1,44 @@ +csv << [ + 'protocol_id', + 'technology_platform', + 'vqa_box_id', + 'sample_group', + 'target_analyte_type', + 'target_organism_name', + 'target_organism_taxonomy_id', + 'pango_lineage', + 'who_label', + 'limit_of_blank', + 'limit_of_blank_unit', + 'limit_of_detection', + 'limit_of_detection_reference_gene', + 'limit_of_detection_unit', + 'limit_of_detection_sn_ratio', + 'limit_of_detection_ci_percent', + 'limit_of_detection_detail', + 'analytical_sensitivity', + 'analytical_sensitivity_unit' +] + +sample = @samples_report.samples[0] +csv << [ + nil, + nil, + sample.box.uuid, + "#{sample.box.purpose}-panel", + 'inactivated virus', + 'SARS-CoV-2', + @target_batch.target_organism_taxonomy_id, + @target_batch.pango_lineage, + @target_batch.who_label, + @samples_report.lob, + nil, + @samples_report.lod, + @target_batch.reference_gene, + nil, + nil, + "90", + 'linear regression', + nil, + nil +] \ No newline at end of file diff --git a/app/views/samples_reports/show.haml b/app/views/samples_reports/show.haml index 0a42cfa3b..3dae2317b 100644 --- a/app/views/samples_reports/show.haml +++ b/app/views/samples_reports/show.haml @@ -294,14 +294,41 @@ document.querySelector(`.nih-report-type-modal-container`).classList.add("hidden"); } - function downloadNIHFiles() { + async function downloadNIHFiles() { hideConfirmModal(); - - // Open the download_nih_tables path in a new tab var context = getUrlParameter('context'); - var url = '/samples_reports/'+#{@samples_report.id}+'/nih_tables?context='+context+""; - window.open(url, '_blank'); - + + // if purpose is challenge, then we need the live threshold calculations + if (#{@purpose.to_json} == "Challenge") { + var threshold = document.getElementById("threshold").value; + var url = '/samples_reports/update_threshold?context='+context+"&samples_report_id=#{@samples_report.id}&threshold="+threshold; + await fetch(url) + .then((response) => response.json()) + .then((ret) => { + var params = { + true_positives: ret.confusion_matrix.true_positive || 0, + true_negatives: ret.confusion_matrix.true_negative || 0, + false_positives: ret.confusion_matrix.false_positive || 0, + false_negatives: ret.confusion_matrix.false_negative || 0, + tpr: (ret.confusion_matrix.true_positive || 0) / ((ret.confusion_matrix.true_positive || 0) + (ret.confusion_matrix.false_negative || 0)), + fpr: (ret.confusion_matrix.false_positive || 0) / ((ret.confusion_matrix.false_positive || 0) + (ret.confusion_matrix.true_negative || 0)), + auc: #{auc(roc_curve(@samples_report))}, + roc_x_values: #{roc_curve(@samples_report).map{|x| x[0]}}, + roc_y_values: #{roc_curve(@samples_report).map{|x| x[1]}}, + }; + + // create url params with the params dictionary + var urlParams = new URLSearchParams(Object.entries(params)); + + // Open the download_nih_tables path in a new tab + var url = '/samples_reports/'+#{@samples_report.id}+'/nih_tables?context='+context+"&"+urlParams.toString(); + window.open(url, '_blank'); + }) + } + else { + var url = '/samples_reports/'+#{@samples_report.id}+'/nih_tables?context='+context; + window.open(url, '_blank'); + } } document.getElementById("print-as-csv").addEventListener("click", showConfirmModal); diff --git a/spec/controllers/nih_tables_controller_spec.rb b/spec/controllers/nih_tables_controller_spec.rb index e090aa0e3..cfe69961a 100644 --- a/spec/controllers/nih_tables_controller_spec.rb +++ b/spec/controllers/nih_tables_controller_spec.rb @@ -11,6 +11,9 @@ @box = Box.make!(:overfilled, institution: @institution, purpose: "LOD") @samples_report = SamplesReport.create(institution: @institution, samples_report_samples: @box.samples.map{|s| SamplesReportSample.new(sample: s)}, name: "Test") + @box2 = Box.make!(:overfilled, institution: @institution, purpose: "Challenge") + @samples_report2 = SamplesReport.create(institution: @institution, samples_report_samples: @box2.samples.map{|s| SamplesReportSample.new(sample: s)}, name: "TestChallenge") + grant @user, @other_user, @other_institution, READ_INSTITUTION end @@ -49,7 +52,6 @@ samples_table = CSV.parse(Zip::File.open_buffer(response.body).entries.find{|e| e.name == "Test_samples.csv"}.get_input_stream.read, headers: true) expect(samples_table.count).to eq(@samples_report.samples_report_samples.count) - expect(samples_table["sample_id"]).to eq(@samples_report.samples_report_samples.map{|srs| srs.sample.id.to_s}) end @@ -59,10 +61,51 @@ samples_table = CSV.parse(Zip::File.open_buffer(response.body).entries.find{|e| e.name == "Test_results.csv"}.get_input_stream.read, headers: true) expect(samples_table.count).to eq(@samples_report.samples_report_samples.count) - expect(samples_table["sample_id"]).to eq(@samples_report.samples_report_samples.map{|srs| srs.sample.id.to_s}) end + it "should contain the LOD table for if the box purpose is LOD" do + get :show, params: { id: @samples_report.id } + + expect(Zip::File.open_buffer(response.body).entries.map(&:name)).to include("Test_lod.csv") + end + + it "should not contain the LOD table for if the box purpose is Challenge" do + get :show, params: { id: @samples_report2.id } + + expect(Zip::File.open_buffer(response.body).entries.map(&:name)).not_to include("TestChallenge_lod.csv") + end + + it "should contain the LOD table with the correct data" do + get :show, params: { id: @samples_report.id } + + lod_table = CSV.parse(Zip::File.open_buffer(response.body).entries.find{|e| e.name == "Test_lod.csv"}.get_input_stream.read, headers: true) + + expect(lod_table.count).to eq(1) + expect(lod_table["vqa_box_id"]).to include(@box.uuid) + end + + it "should contain the Challenge table for if the box purpose is Challenge" do + get :show, params: { id: @samples_report2.id } + + expect(Zip::File.open_buffer(response.body).entries.map(&:name)).to include("TestChallenge_challenge.csv") + end + + it "should not contain the Challenge table for if the box purpose is LOD" do + get :show, params: { id: @samples_report.id } + + expect(Zip::File.open_buffer(response.body).entries.map(&:name)).not_to include("Test_challenge.csv") + end + + it "should contain the Challenge table with the correct data" do + get :show, params: { id: @samples_report2.id } + + challenge_table = CSV.parse(Zip::File.open_buffer(response.body).entries.find{|e| e.name == "TestChallenge_challenge.csv"}.get_input_stream.read, headers: true) + + expect(challenge_table.count).to eq(1) + expect(challenge_table["vqa_box_id"]).to include(@box2.uuid) + end + it "should not allow access to user without read access" do sign_in @other_user get :show, params: { id: @samples_report.id, context: @other_institution.uuid }