Skip to content

Commit

Permalink
Merge pull request #1975: various fixes for boxes & samples reports
Browse files Browse the repository at this point in the history
  • Loading branch information
ysbaddaden authored Jul 24, 2023
2 parents 7ab2b63 + f22d6b2 commit 50cdc80
Show file tree
Hide file tree
Showing 27 changed files with 326 additions and 250 deletions.
10 changes: 8 additions & 2 deletions app/assets/javascripts/components/cdx_select_autocomplete.js.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
var CdxSelectAutocomplete = React.createClass({
componentWillMount: function () {
this.asyncOptions = _.debounce(this.asyncOptions.bind(this), 100, { maxWait: 250 });
getDefaultProps: function() {
return {
className: "input-large"
};
},

getInitialState: function () {
Expand All @@ -9,6 +11,10 @@ var CdxSelectAutocomplete = React.createClass({
};
},

componentWillMount: function () {
this.asyncOptions = _.debounce(this.asyncOptions.bind(this), 100, { maxWait: 250 });
},

// Since <Select> uses a different INPUT we must copy the value when the
// user is typing from the visible to the hidden INPUT, otherwise when the
// user enters free-text of validates with an invalid value, the backend won't
Expand Down
29 changes: 11 additions & 18 deletions app/assets/javascripts/components/upload_csv_box.js.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ var UploadCsvBox = React.createClass({
getInitialState: function() {
return {
csrfToken: this.props.csrf_token,
url: "/csv_validation/" + this.props.context,
url: this.props.validate_url,
fieldName: this.props.name, // Initial fieldName value
uploadRows: [], // Array to store upload rows
hideListItems: "hidden",
Expand All @@ -16,7 +16,7 @@ var UploadCsvBox = React.createClass({
this.setState({ fileValue: event.target.value });
const file = event.target.files[0];
const formData = new FormData();
formData.append('csv_file', file);
formData.append('csv_box', file);

fetch(this.state.url, {
method: "POST",
Expand All @@ -33,25 +33,19 @@ var UploadCsvBox = React.createClass({
}
})
.then(responseJson => {
const not_found_batches = responseJson.not_found_batches;
const samples_nbr = responseJson.samples_nbr;
const error_message = responseJson.error_message;

// Create row from template with filename and upload info
const filename = file.name;
const uploadInfo = {
uploadedSamplesCount: samples_nbr,
notFoundUuids: not_found_batches,
errorMessage: error_message
const uploadInfo = {
uploadedSamplesCount: responseJson.samples_count,
notFoundUuids: responseJson.not_found_batches,
errorMessage: responseJson.error_message,
};

// Add the new row to the state
this.setState(prevState => ({
uploadRows: [...prevState.uploadRows, {filename, uploadInfo, showTooltip: false}]
uploadRows: [...prevState.uploadRows, {filename: file.name, uploadInfo, showTooltip: false}]
}));

this.setState({ hideListItems: "" });

})
.catch(error => {
this.setState({ errorMessage: error });
Expand Down Expand Up @@ -97,7 +91,7 @@ var UploadCsvBox = React.createClass({

const tooltipText = notFoundUuids.slice(0, 5).map((batch_number) => <div>{batch_number}</div>);

const rowContent = errorMessage == "" ?
const rowContent = errorMessage ?
<div className="uploaded-samples-count">
{uploadedSamplesCount} {samplesText}
{batchesNotFound > 0 && (
Expand All @@ -110,17 +104,16 @@ var UploadCsvBox = React.createClass({
{errorMessage}
</div>;


return (
<div className="items-row" key={filename}>
<div className="items-item gap-5">
<div className="icon-circle-minus icon-gray remove_file" onClick={() => this.handleRemove(index)}></div>
<div className="file-name">{filename}</div>
</div>
<div className={`items-row-action gap-5 not_found_message ${batchesNotFound > 0 ? ' ttip ' : ' '} ${batchesNotFound > 0 || errorMessage != "" ? ' input-required ' : ' '}}`}
<div className={`items-row-action gap-5 not_found_message ${batchesNotFound > 0 ? ' ttip ' : ' '} ${batchesNotFound > 0 || errorMessage ? ' input-required ' : ' '}}`}
onClick={() => this.handleClick(index)}>
{rowContent}
<div className={`upload-icon bigger ${batchesNotFound > 0 || errorMessage != "" ? 'icon-alert icon-red' : 'icon-check'}`}></div>
{rowContent}
<div className={`upload-icon bigger ${batchesNotFound > 0 || errorMessage ? 'icon-alert icon-red' : 'icon-check'}`}></div>
{batchesNotFound > 0 && (
<div className={`ttext not-found-uuids ${showTooltip ? '' : 'hidden'}`}>
{tooltipText}
Expand Down
29 changes: 28 additions & 1 deletion app/controllers/boxes_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class BoxesController < ApplicationController
before_action :load_box, except: %i[index new create bulk_destroy]
before_action :load_box, except: %i[index new validate create bulk_destroy]

def index
@can_create = has_access?(@navigation_context.institution, CREATE_INSTITUTION_BOX)
Expand Down Expand Up @@ -51,6 +51,33 @@ def new
@box_form = BoxForm.build(@navigation_context)
end

def validate
@samples_count = 0
batch_numbers = Set.new

# TODO: should be handled by BoxForm (& duplicates BoxForm#parse_csv)
CSV.open(params[:csv_box].path, headers: true) do |csv|
while csv.headers == true
csv.readline
end

unless csv.headers == ["Batch", "Concentration", "Distractor", "Instructions"]
@error_message = "Invalid columns"
return # rubocop:disable Lint/NonLocalExitFromIterator
end

csv.each do |row|
if batch_number = row["Batch"].presence&.strip
batch_numbers << batch_number
@samples_count += 1
end
end
end

@found_batches = @navigation_context.institution.batches.where(batch_number: batch_numbers.to_a).pluck(:batch_number)
@not_found_batches = (batch_numbers - @found_batches)
end

def create
return unless authorize_resource(@navigation_context.institution, CREATE_INSTITUTION_BOX)

Expand Down
36 changes: 0 additions & 36 deletions app/controllers/csv_validations_controller.rb

This file was deleted.

26 changes: 13 additions & 13 deletions app/controllers/nih_tables_controller.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
class NihTablesController < ApplicationController
def show
@samples_report = SamplesReport.find(params[:id])
@target_batch = @samples_report.target_batch
@samples_report = SamplesReport.preload(:samples => :box).find(params[:id])
return unless authorize_resource(@samples_report, READ_SAMPLES_REPORT)

zip_data = create_zip_file

@target_box = @samples_report.boxes.take
@target_sample = @samples_report.target_sample
zip_data = create_zip_file(@target_box.purpose)
send_data zip_data.read, type: 'application/zip', filename: "#{@samples_report.name}_nih_tables.zip"
end

private

def create_zip_file
purpose = @samples_report.samples[0].box.purpose

def create_zip_file(purpose)
zip_stream = Zip::OutputStream.write_buffer do |stream|
# Read public/templates/Instructions.txt contents and write to zip
# TODO: read public/templates/Instructions.txt contents and write to zip
stream.put_next_entry('Instructions.txt')
stream.write(File.read(Rails.root.join('public/templates/Instructions.txt')))

add_nih_table('samples', stream)
add_nih_table('results', stream)

if purpose == "LOD"
case purpose
when "LOD"
add_nih_table('lod', stream)
elsif purpose == "Challenge"
when "Challenge"
add_nih_table('challenge', stream)
end
end
Expand All @@ -33,6 +33,6 @@ def create_zip_file

def add_nih_table(table_name, stream)
stream.put_next_entry("#{@samples_report.name}_#{table_name}.csv")
stream.write(render_to_string('samples_reports/nih_'+table_name, formats: :csv))
stream.write(render_to_string("nih_#{table_name}", formats: :csv))
end
end
end
12 changes: 11 additions & 1 deletion app/controllers/samples_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,22 +201,32 @@ def bulk_destroy
redirect_to samples_path, notice: "Samples were successfully deleted."
end

def upload_results
end

def bulk_process_csv
uuid_regex = /\b[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}\b/i
samples = check_access(@navigation_context.institution.samples, UPDATE_SAMPLE)
box_ids = Set.new

params[:csv_files].each do |csv_file|
CSV.open(csv_file.path) do |csv_stream|
csv_stream.each do |(sample_id, measured_signal)|
next unless sample_id&.match(uuid_regex) && measured_signal.present?

if sample = Sample.find_by_uuid(sample_id.strip)
if sample = samples.find_by_uuid(sample_id.strip)
box_ids << sample.box_id
sample.measured_signal ||= Float(measured_signal.strip)
sample.save!
end
end
end
end

Box.where(blinded: true, id: box_ids.to_a).each do |box|
box.unblind! if box.samples.all?(&:measured_signal)
end

redirect_to samples_path, notice: "Sample's results uploaded successfully."
end

Expand Down
5 changes: 2 additions & 3 deletions app/controllers/samples_reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ def index
@samples_reports = SamplesReport.where(institution: @navigation_context.institution)
@samples_reports = check_access(@samples_reports, READ_SAMPLES_REPORT).order('samples_reports.created_at DESC')

# Filter by search params

# filter by search params
@samples_reports = @samples_reports.partial_name(params[:name])
@samples_reports = @samples_reports.partial_sample_uuid(params[:sample_uuid])
@samples_reports = @samples_reports.partial_box_uuid(params[:box_uuid])
@samples_reports = @samples_reports.partial_batch_number(params[:batch_number])

#paginate samples report
# paginate samples report
@samples_reports = perform_pagination(@samples_reports)
end

Expand Down
16 changes: 9 additions & 7 deletions app/helpers/autocomplete_field_helper.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
module AutocompleteFieldHelper
def autocomplete_field(f, field, institution)
field_name = field.match(/\[(.*?)\]/)[1]
react_component "CdxSelectAutocomplete", {
name: field,
value: f.object.public_send(field_name.to_sym),
def autocomplete_field(f, name, field_name, institution, **attributes)
class_name = attributes.delete(:class)
props = attributes.merge(
name: name,
value: f.object.public_send(field_name),
url: institution_autocomplete_path(institution_id: institution.id, field_name: field_name),
combobox: true,
}
combobox: true
)
props[:className] = class_name if class_name
react_component("CdxSelectAutocomplete", props)
end
end
37 changes: 19 additions & 18 deletions app/models/box_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ def initialize(box, params)
@option = params[:option]
@media = params[:media].presence
@batches_data = params[:batches].presence.to_h
@csv_box = params[:csv_box].presence
initialize_csv_box if @csv_box
if uploaded_file = params[:csv_box].presence
parse_csv(uploaded_file.path)
end
@batch_uuids = @batches_data.transform_values { |v| v[:batch_uuid] }
@sample_uuids = params[:sample_uuids].presence.to_h
end
Expand Down Expand Up @@ -204,23 +205,23 @@ def have_distractor_sample?
@samples.any? { |_, sample| sample.distractor }
end

def initialize_csv_box
CSV.open(@csv_box.path) do |csv_stream|
i = 0
csv_stream.each do |row|
batch_number, concentration, distractor, instruction = row[0..3]
batch_uuid = Batch.find_by(batch_number: batch_number)&.uuid
next if batch_uuid.blank?
@batches_data[i] = {
batch_uuid: batch_uuid,
distractor: distractor.downcase == "yes",
instruction: instruction,
concentrations: {"0" => {
replicate: 1,
concentration: Integer(Float(concentration)),
}},
def parse_csv(path)
CSV.open(path, headers: true) do |csv|
csv.each do |row|
next unless batch_number = row["Batch"]&.strip.presence
next unless batch = @box.institution.batches.find_by(batch_number: batch_number)

@batches_data[@batches_data.size] = {
batch_uuid: batch.uuid,
distractor: row["Distractor"]&.strip&.downcase == "yes",
instruction: row["Instructions"],
concentrations: {
"0" => {
replicate: 1,
concentration: Integer(Float(row["Concentration"]&.strip)),
},
},
}
i += 1
end
end
end
Expand Down
3 changes: 2 additions & 1 deletion app/models/sample.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def self.institution_is_required
has_many :sample_identifiers, inverse_of: :sample, dependent: :destroy
has_many :test_results, through: :sample_identifiers

has_many :samples_reports, through: :samples_report_sample
has_many :samples_reports, through: :samples_report_samples
has_many :samples_report_samples, dependent: :destroy

has_many :assay_attachments, dependent: :destroy
accepts_nested_attributes_for :assay_attachments, allow_destroy: true
Expand Down
Loading

0 comments on commit 50cdc80

Please sign in to comment.