diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/duplex_seq/row.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/duplex_seq/row.rb new file mode 100644 index 000000000..f19391b28 --- /dev/null +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/duplex_seq/row.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# Part of the Labware creator classes +module LabwareCreators + require_dependency 'labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq' + + module PcrCyclesBinnedPlate::CsvFile + # + # This version of the row is for the Duplex Seq pipeline. + # + class DuplexSeq::Row < RowBase + include ActiveModel::Validations + + SUB_POOL_NOT_BLANK = 'has a value when Submit for Sequencing is N, it should be empty, in %s' + SUBMIT_FOR_SEQ_INVALID = 'is empty or has an unrecognised value (should be Y or N), in %s' + COVERAGE_MISSING = 'is missing but should be present when Submit for Sequencing is Y, in %s' + COVERAGE_NEGATIVE = 'is negative but should be a positive value, in %s' + + attr_reader :submit_for_sequencing, :sub_pool, :coverage + + validate :submit_for_sequencing_has_expected_value + validate :sub_pool_within_expected_range + validates :coverage, + presence: { + message: ->(object, _data) { COVERAGE_MISSING % object } + }, + numericality: { + greater_than: 0, + message: ->(object, _data) { COVERAGE_NEGATIVE % object } + }, + unless: -> { empty? || !submit_for_sequencing? } + + delegate :submit_for_sequencing_column, :sub_pool_column, :coverage_column, to: :header + + def initialize_pipeline_specific_columns + @submit_for_sequencing_as_string = @row_data[submit_for_sequencing_column]&.strip&.upcase + @sub_pool = @row_data[sub_pool_column]&.strip&.to_i + @coverage = @row_data[coverage_column]&.strip&.to_i + end + + def submit_for_sequencing? + @submit_for_sequencing ||= (@submit_for_sequencing_as_string == 'Y') + end + + def submit_for_sequencing_has_expected_value + return if empty? + + return if %w[Y N].include? @submit_for_sequencing_as_string + + errors.add('submit_for_sequencing', format(SUBMIT_FOR_SEQ_INVALID, to_s)) + end + + def sub_pool_within_expected_range + return if empty? + + # check the value is within range when we do expect a value to be present + if submit_for_sequencing? + in_range('sub_pool', sub_pool, @row_config.sub_pool_min, @row_config.sub_pool_max) + else + # expect sub-pool field to be blank, possible mistake by user if not + return if sub_pool.blank? + + # sub-pool is NOT blank and should be + errors.add('sub_pool', format(SUB_POOL_NOT_BLANK, to_s)) + end + end + end + end +end diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/duplex_seq/well_details_header.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/duplex_seq/well_details_header.rb new file mode 100644 index 000000000..67e70d6df --- /dev/null +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/duplex_seq/well_details_header.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Part of the Labware creator classes +module LabwareCreators + require_dependency 'labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq' + + module PcrCyclesBinnedPlate::CsvFile + # + # Class WellDetailsHeader provides a simple wrapper for handling and validating + # the plate barcode header row from the customer csv file + # + class DuplexSeq::WellDetailsHeader < WellDetailsHeaderBase + # Return the index of the respective column. + attr_reader :submit_for_sequencing_column, :sub_pool_column, :coverage_column + + SUBMIT_FOR_SEQUENCING_COLUMN = 'Submit for sequencing (Y/N)?' + SUB_POOL_COLUMN = 'Sub-Pool' + COVERAGE_COLUMN = 'Coverage' + NOT_FOUND = 'could not be found in: ' + + validates :submit_for_sequencing_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + validates :sub_pool_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + validates :coverage_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + + private + + def initialize_pipeline_specific_columns + @submit_for_sequencing_column = index_of_header(SUBMIT_FOR_SEQUENCING_COLUMN) + @sub_pool_column = index_of_header(SUB_POOL_COLUMN) + @coverage_column = index_of_header(COVERAGE_COLUMN) + end + end + end +end diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/plate_barcode_header.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/plate_barcode_header.rb index b3e9cfc09..ccc689c80 100644 --- a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/plate_barcode_header.rb +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/plate_barcode_header.rb @@ -2,7 +2,7 @@ # Part of the Labware creator classes module LabwareCreators - require_dependency 'labware_creators/custom_pooled_tubes/csv_file' + require_dependency 'labware_creators/pcr_cycles_binned_plate/csv_file_base' # # Class PlateBarcodeHeader provides a simple wrapper for handling and validating @@ -22,9 +22,10 @@ class PcrCyclesBinnedPlate::CsvFile::PlateBarcodeHeader BARCODE_NOT_MATCHING = 'The plate barcode in the file (%s) does not match the barcode of ' \ 'the plate being uploaded to (%s), please check you have the correct file.' + NOT_FOUND = 'could not be found in: ' - validates :barcode_lbl_index, presence: { message: ->(object, _data) { "could not be found in: '#{object}'" } } - validates :plate_barcode, presence: { message: ->(object, _data) { "could not be found in: '#{object}'" } } + validates :barcode_lbl_index, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + validates :plate_barcode, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } validate :plate_barcode_matches_parent? # diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/row.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/row.rb deleted file mode 100644 index 6b7a12b7a..000000000 --- a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/row.rb +++ /dev/null @@ -1,173 +0,0 @@ -# frozen_string_literal: true - -# Part of the Labware creator classes -module LabwareCreators - require_dependency 'labware_creators/pcr_cycles_binned_plate/csv_file' - - # - # Class CsvRow provides a simple wrapper for handling and validating - # individual CSV rows - # - class PcrCyclesBinnedPlate::CsvFile::Row # rubocop:todo Metrics/ClassLength - include ActiveModel::Validations - - IN_RANGE = 'is empty or contains a value that is out of range (%s to %s), in %s' - SUB_POOL_NOT_BLANK = 'has a value when Submit for Sequencing is N, it should be empty, in %s' - SUBMIT_FOR_SEQ_INVALID = 'is empty or has an unrecognised value (should be Y or N), in %s' - COVERAGE_MISSING = 'is missing but should be present when Submit for Sequencing is Y, in %s' - COVERAGE_NEGATIVE = 'is negative but should be a positive value, in %s' - WELL_NOT_RECOGNISED = 'contains an invalid well name: %s' - - attr_reader :header, - :well, - :concentration, - :sanger_sample_id, - :supplier_sample_name, - :input_amount_available, - :input_amount_desired, - :sample_volume, - :diluent_volume, - :pcr_cycles, - :submit_for_sequencing, - :sub_pool, - :coverage, - :index - - validates :well, - inclusion: { - in: WellHelpers.column_order, - message: ->(object, _data) { WELL_NOT_RECOGNISED % object } - }, - unless: :empty? - validate :input_amount_desired_within_expected_range? - validate :sample_volume_within_expected_range? - validate :diluent_volume_within_expected_range? - validate :pcr_cycles_within_expected_range? - validate :submit_for_sequencing_has_expected_value? - validate :sub_pool_within_expected_range? - validates :coverage, - presence: { - message: ->(object, _data) { COVERAGE_MISSING % object } - }, - numericality: { - greater_than: 0, - message: ->(object, _data) { COVERAGE_NEGATIVE % object } - }, - unless: -> { empty? || !submit_for_sequencing? } - delegate :well_column, - :concentration_column, - :sanger_sample_id_column, - :supplier_sample_name_column, - :input_amount_available_column, - :input_amount_desired_column, - :sample_volume_column, - :diluent_volume_column, - :pcr_cycles_column, - :submit_for_sequencing_column, - :sub_pool_column, - :coverage_column, - to: :header - - # rubocop:todo Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity - def initialize(row_config, header, index, row_data) - @row_config = row_config - @header = header - @index = index - @row_data = row_data - - # initialize supplied fields - @well = (@row_data[well_column] || '').strip.upcase - @concentration = @row_data[concentration_column]&.strip&.to_f - @sanger_sample_id = @row_data[sanger_sample_id_column]&.strip - @supplier_sample_name = (@row_data[supplier_sample_name_column])&.strip - @input_amount_available = @row_data[input_amount_available_column]&.strip&.to_f - - # initialize customer fields - @input_amount_desired = @row_data[input_amount_desired_column]&.strip&.to_f - @sample_volume = @row_data[sample_volume_column]&.strip&.to_f - @diluent_volume = @row_data[diluent_volume_column]&.strip&.to_f - @pcr_cycles = @row_data[pcr_cycles_column]&.strip&.to_i - @submit_for_sequencing_as_string = @row_data[submit_for_sequencing_column]&.strip&.upcase - @sub_pool = @row_data[sub_pool_column]&.strip&.to_i - @coverage = @row_data[coverage_column]&.strip&.to_i - end - - # rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity - - def submit_for_sequencing? - @submit_for_sequencing ||= (@submit_for_sequencing_as_string == 'Y') - end - - def to_s - @well.present? ? "row #{index + 2} [#{@well}]" : "row #{index + 2}" - end - - def input_amount_desired_within_expected_range? - in_range?( - 'input_amount_desired', - input_amount_desired, - @row_config.input_amount_desired_min, - @row_config.input_amount_desired_max - ) - end - - def sample_volume_within_expected_range? - in_range?('sample_volume', sample_volume, @row_config.sample_volume_min, @row_config.sample_volume_max) - end - - def diluent_volume_within_expected_range? - in_range?('diluent_volume', diluent_volume, @row_config.diluent_volume_min, @row_config.diluent_volume_max) - end - - def pcr_cycles_within_expected_range? - in_range?('pcr_cycles', pcr_cycles, @row_config.pcr_cycles_min, @row_config.pcr_cycles_max) - end - - def submit_for_sequencing_has_expected_value? - return true if empty? - - return true if %w[Y N].include? @submit_for_sequencing_as_string - - errors.add('submit_for_sequencing', format(SUBMIT_FOR_SEQ_INVALID, to_s)) - end - - def sub_pool_within_expected_range? - return true if empty? - - # check the value is within range when we do expect a value to be present - if submit_for_sequencing? - return in_range?('sub_pool', sub_pool, @row_config.sub_pool_min, @row_config.sub_pool_max) - end - - # expect sub-pool field to be blank, possible mistake by user if not - return true if sub_pool.blank? - - # sub-pool is NOT blank and should be - errors.add('sub_pool', format(SUB_POOL_NOT_BLANK, to_s)) - false - end - - # Checks whether a row value it within the specified range using min/max values - # from the row config - # - # field_name [string] The name of the field being validated - # field_value [float or int] The value being tested - # min/max [float or int] The minimum and maximum in the range - # - # @return [bool] - def in_range?(field_name, field_value, min, max) - return true if empty? - - result = (min..max).cover? field_value - unless result - msg = format(IN_RANGE, min, max, to_s) - errors.add(field_name, msg) - end - result - end - - def empty? - @row_data.empty? || @row_data.compact.empty? || sanger_sample_id.blank? - end - end -end diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/row_base.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/row_base.rb new file mode 100644 index 000000000..ff3872ebe --- /dev/null +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/row_base.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +# Part of the Labware creator classes +module LabwareCreators + # + # Provides a simple wrapper for handling and validating individual CSV rows. + # Abstract class, extend for uses in specific pipelines. + # + class PcrCyclesBinnedPlate::CsvFile::RowBase + include ActiveModel::Validations + + IN_RANGE = 'is empty or contains a value that is out of range (%s to %s), in %s' + WELL_NOT_RECOGNISED = 'contains an invalid well name: %s' + + attr_reader :header, + :well, + :concentration, + :sanger_sample_id, + :supplier_sample_name, + :input_amount_available, + :input_amount_desired, + :sample_volume, + :diluent_volume, + :pcr_cycles, + :index + + validates :well, + inclusion: { + in: WellHelpers.column_order, + message: ->(object, _data) { WELL_NOT_RECOGNISED % object } + }, + unless: :empty? + validate :input_amount_desired_within_expected_range + validate :sample_volume_within_expected_range + validate :diluent_volume_within_expected_range + validate :pcr_cycles_within_expected_range + + delegate :well_column, + :concentration_column, + :sanger_sample_id_column, + :supplier_sample_name_column, + :input_amount_available_column, + :input_amount_desired_column, + :sample_volume_column, + :diluent_volume_column, + :pcr_cycles_column, + to: :header + + # rubocop:todo Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + def initialize(row_config, header, index, row_data) + @row_config = row_config + @header = header + @index = index + @row_data = row_data + + # initialize supplied fields + @well = (@row_data[well_column] || '').strip.upcase + @concentration = @row_data[concentration_column]&.strip&.to_f + @sanger_sample_id = @row_data[sanger_sample_id_column]&.strip + @supplier_sample_name = (@row_data[supplier_sample_name_column])&.strip + @input_amount_available = @row_data[input_amount_available_column]&.strip&.to_f + + # initialize customer fields + @input_amount_desired = @row_data[input_amount_desired_column]&.strip&.to_f + @sample_volume = @row_data[sample_volume_column]&.strip&.to_f + @diluent_volume = @row_data[diluent_volume_column]&.strip&.to_f + @pcr_cycles = @row_data[pcr_cycles_column]&.strip&.to_i + + initialize_pipeline_specific_columns + end + + # rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + + def initialize_pipeline_specific_columns + raise '#initialize_pipeline_specific_columns must be implemented on subclasses' + end + + def to_s + @well.present? ? "row #{index + 2} [#{@well}]" : "row #{index + 2}" + end + + def input_amount_desired_within_expected_range + in_range( + 'input_amount_desired', + input_amount_desired, + @row_config.input_amount_desired_min, + @row_config.input_amount_desired_max + ) + end + + def sample_volume_within_expected_range + in_range('sample_volume', sample_volume, @row_config.sample_volume_min, @row_config.sample_volume_max) + end + + def diluent_volume_within_expected_range + in_range('diluent_volume', diluent_volume, @row_config.diluent_volume_min, @row_config.diluent_volume_max) + end + + def pcr_cycles_within_expected_range + in_range('pcr_cycles', pcr_cycles, @row_config.pcr_cycles_min, @row_config.pcr_cycles_max) + end + + # Checks whether a row value it within the specified range using min/max values + # from the row config + # + # field_name [string] The name of the field being validated + # field_value [float or int] The value being tested + # min/max [float or int] The minimum and maximum in the range + def in_range(field_name, field_value, min, max) + return if empty? + + return if (min..max).cover? field_value + + msg = format(IN_RANGE, min, max, to_s) + errors.add(field_name, msg) + end + + def empty? + @row_data.empty? || @row_data.compact.empty? || sanger_sample_id.blank? + end + end +end diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/t_nano_seq/row.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/t_nano_seq/row.rb new file mode 100644 index 000000000..8f382a996 --- /dev/null +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/t_nano_seq/row.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# Part of the Labware creator classes +module LabwareCreators + require_dependency 'labware_creators/pcr_cycles_binned_plate/csv_file_for_t_nano_seq' + + module PcrCyclesBinnedPlate::CsvFile + # + # This version of the row is for the Targeted NanoSeq pipeline. + # + class TNanoSeq::Row < RowBase + include ActiveModel::Validations + + HYB_PANEL_MISSING = 'is empty, in %s' + + attr_reader :hyb_panel + + validate :hyb_panel_is_present + + delegate :hyb_panel_column, to: :header + + def initialize_pipeline_specific_columns + @hyb_panel = @row_data[hyb_panel_column]&.strip + end + + # Checks whether the Hyb Panel column is filled in + def hyb_panel_is_present + return if empty? + + # TODO: can we validate the hyb panel value? Does not appear to be tracked in LIMS. + return if hyb_panel.present? + + msg = format(HYB_PANEL_MISSING, to_s) + errors.add('hyb_panel', msg) + end + end + end +end diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/t_nano_seq/well_details_header.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/t_nano_seq/well_details_header.rb new file mode 100644 index 000000000..d6e5655c8 --- /dev/null +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/t_nano_seq/well_details_header.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Part of the Labware creator classes +module LabwareCreators + require_dependency 'labware_creators/pcr_cycles_binned_plate/csv_file_for_t_nano_seq' + + module PcrCyclesBinnedPlate::CsvFile + # + # Class WellDetailsHeader provides a simple wrapper for handling and validating + # the plate barcode header row from the customer csv file + # This version is for the Targeted NanoSeq pipeline. + # + class TNanoSeq::WellDetailsHeader < WellDetailsHeaderBase + include ActiveModel::Validations + + # Return the index of the respective column. + attr_reader :hyb_panel_column + + HYB_PANEL_COLUMN = 'Hyb Panel' + NOT_FOUND = 'could not be found in: ' + + validates :hyb_panel_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + + private + + def initialize_pipeline_specific_columns + @hyb_panel_column = index_of_header(HYB_PANEL_COLUMN) + end + end + end +end diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/well_details_header.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/well_details_header_base.rb similarity index 59% rename from app/models/labware_creators/pcr_cycles_binned_plate/csv_file/well_details_header.rb rename to app/models/labware_creators/pcr_cycles_binned_plate/csv_file/well_details_header_base.rb index 46ae8922d..9b36489f6 100644 --- a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/well_details_header.rb +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/well_details_header_base.rb @@ -2,16 +2,15 @@ # Part of the Labware creator classes module LabwareCreators - require_dependency 'labware_creators/custom_pooled_tubes/csv_file' - # # Class WellDetailsHeader provides a simple wrapper for handling and validating # the plate barcode header row from the customer csv file # - class PcrCyclesBinnedPlate::CsvFile::WellDetailsHeader + class PcrCyclesBinnedPlate::CsvFile::WellDetailsHeaderBase include ActiveModel::Validations # Return the index of the respective column. + # These are common columns shared by all versions of the csv file. attr_reader :well_column, :concentration_column, :sanger_sample_id_column, @@ -20,10 +19,7 @@ class PcrCyclesBinnedPlate::CsvFile::WellDetailsHeader :input_amount_desired_column, :sample_volume_column, :diluent_volume_column, - :pcr_cycles_column, - :submit_for_sequencing_column, - :sub_pool_column, - :coverage_column + :pcr_cycles_column WELL_COLUMN = 'Well' CONCENTRATION_COLUMN = 'Concentration (nM)' @@ -34,44 +30,24 @@ class PcrCyclesBinnedPlate::CsvFile::WellDetailsHeader SAMPLE_VOLUME_COLUMN = 'Sample volume' DILUENT_VOLUME_COLUMN = 'Diluent volume' PCR_CYCLES_COLUMN = 'PCR cycles' - SUBMIT_FOR_SEQUENCING_COLUMN = 'Submit for sequencing (Y/N)?' - SUB_POOL_COLUMN = 'Sub-Pool' - COVERAGE_COLUMN = 'Coverage' + NOT_FOUND = 'could not be found in: ' - validates :well_column, presence: { message: ->(object, _data) { "could not be found in: '#{object}'" } } - validates :concentration_column, presence: { message: ->(object, _data) { "could not be found in: '#{object}'" } } - validates :sanger_sample_id_column, - presence: { - message: ->(object, _data) { "could not be found in: '#{object}'" } - } - validates :supplier_sample_name_column, - presence: { - message: ->(object, _data) { "could not be found in: '#{object}'" } - } - validates :input_amount_available_column, - presence: { - message: ->(object, _data) { "could not be found in: '#{object}'" } - } - validates :input_amount_desired_column, - presence: { - message: ->(object, _data) { "could not be found in: '#{object}'" } - } - validates :sample_volume_column, presence: { message: ->(object, _data) { "could not be found in: '#{object}'" } } - validates :diluent_volume_column, presence: { message: ->(object, _data) { "could not be found in: '#{object}'" } } - validates :pcr_cycles_column, presence: { message: ->(object, _data) { "could not be found in: '#{object}'" } } - validates :submit_for_sequencing_column, - presence: { - message: ->(object, _data) { "could not be found in: '#{object}'" } - } - validates :sub_pool_column, presence: { message: ->(object, _data) { "could not be found in: '#{object}'" } } - validates :coverage_column, presence: { message: ->(object, _data) { "could not be found in: '#{object}'" } } + validates :well_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + validates :concentration_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + validates :sanger_sample_id_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + validates :supplier_sample_name_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + validates :input_amount_available_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + validates :input_amount_desired_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + validates :sample_volume_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + validates :diluent_volume_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } + validates :pcr_cycles_column, presence: { message: ->(object, _data) { "#{NOT_FOUND}'#{object}'" } } # # Generates a well details header from the well details header row array # # @param [Array] row The array of fields extracted from the CSV file # - def initialize(row) # rubocop:todo Metrics/AbcSize + def initialize(row) @row = row || [] @well_column = index_of_header(WELL_COLUMN) @@ -83,9 +59,8 @@ def initialize(row) # rubocop:todo Metrics/AbcSize @sample_volume_column = index_of_header(SAMPLE_VOLUME_COLUMN) @diluent_volume_column = index_of_header(DILUENT_VOLUME_COLUMN) @pcr_cycles_column = index_of_header(PCR_CYCLES_COLUMN) - @submit_for_sequencing_column = index_of_header(SUBMIT_FOR_SEQUENCING_COLUMN) - @sub_pool_column = index_of_header(SUB_POOL_COLUMN) - @coverage_column = index_of_header(COVERAGE_COLUMN) + + initialize_pipeline_specific_columns end # @@ -99,6 +74,10 @@ def to_s private + def initialize_pipeline_specific_columns + raise '#initialize_pipeline_specific_columns must be implemented on subclasses' + end + # # Returns the index of the given column name. Returns nil if the column can't be found. # Uses strip and case insensitive matching diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_base.rb similarity index 76% rename from app/models/labware_creators/pcr_cycles_binned_plate/csv_file.rb rename to app/models/labware_creators/pcr_cycles_binned_plate/csv_file_base.rb index 180082f58..11faeeed3 100644 --- a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file.rb +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_base.rb @@ -5,15 +5,16 @@ # Part of the Labware creator classes module LabwareCreators - require_dependency 'labware_creators/pcr_cycles_binned_plate' + require_dependency 'labware_creators/pcr_cycles_binned_plate_base' # # Takes the user uploaded csv file, validates the content and extracts the well information. # This file will be downloaded from Limber based on the quantification results, then sent out # to and filled in by the customer. It describes how to dilute and bin the samples together # in the child dilution plate. + # This is the abstract version of this labware creator, extend from this class # - class PcrCyclesBinnedPlate::CsvFile + class PcrCyclesBinnedPlate::CsvFileBase include ActiveModel::Validations extend NestedValidation @@ -33,11 +34,11 @@ class PcrCyclesBinnedPlate::CsvFile :sample_volume_column, :diluent_volume_column, :pcr_cycles_column, - :submit_for_sequencing_column, - :sub_pool_column, - :coverage_column, to: :well_details_header_row + # implement on subclasses + FIELDS_FOR_WELL_DETAILS = [].freeze + # # Passing in the file to be parsed, the configuration that holds validation range thresholds, and # the parent plate barcode for validation that we are processing the correct file. @@ -51,7 +52,7 @@ def initialize(file, config, parent_barcode) end def initialize_variables(file, config, parent_barcode) - @config = Utility::PcrCyclesCsvFileUploadConfig.new(config) + @config = get_config_details_from_purpose(config) @parent_barcode = parent_barcode @data = CSV.parse(file.read) remove_bom @@ -83,16 +84,22 @@ def correctly_parsed? end def plate_barcode_header_row - @plate_barcode_header_row ||= PlateBarcodeHeader.new(@parent_barcode, @data[0]) if @data[0] + # data[0] here is the first row in the uploaded file, and should contain the plate barcode + @plate_barcode_header_row ||= + PcrCyclesBinnedPlate::CsvFile::PlateBarcodeHeader.new(@parent_barcode, @data[0]) if @data[0] end # Returns the contents of the header row for the well detail columns def well_details_header_row - @well_details_header_row ||= WellDetailsHeader.new(@data[2]) if @data[2] + raise '#well_details_header_row must be implemented on subclasses' end private + def get_config_details_from_purpose(_config) + raise '#get_config_details_from_purpose must be implemented on subclasses' + end + # remove byte order marker if present def remove_bom return unless @data.present? && @data[0][0].present? @@ -108,10 +115,12 @@ def remove_bom end def transfers - @transfers ||= - @data[3..].each_with_index.map do |row_data, index| - Row.new(@config, well_details_header_row, index + 2, row_data) - end + # sample row data starts on third row of file, 1st row is plate barcode header row, second blank + @transfers ||= @data[3..].each_with_index.map { |row_data, index| create_row(index, row_data) } + end + + def create_row(_index, _row_data) + raise '#create_row must be implemented on subclasses' end # Gates looking for wells if the file is invalid @@ -123,7 +132,8 @@ def correctly_formatted? def generate_well_details_hash return {} unless valid? - fields = %w[diluent_volume pcr_cycles submit_for_sequencing sub_pool coverage sample_volume] + fields = self.class::FIELDS_FOR_WELL_DETAILS + transfers.each_with_object({}) do |row, well_details_hash| next if row.empty? diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq.rb new file mode 100644 index 000000000..24121d6e2 --- /dev/null +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require './lib/nested_validation' +require 'csv' + +# Part of the Labware creator classes +module LabwareCreators + require_dependency 'labware_creators/pcr_cycles_binned_plate_for_duplex_seq' + + module PcrCyclesBinnedPlate + # + # This version of the csv file is for Duplex Seq. + # + class CsvFileForDuplexSeq < CsvFileBase + delegate :submit_for_sequencing_column, :sub_pool_column, :coverage_column, to: :well_details_header_row + + FIELDS_FOR_WELL_DETAILS = %w[diluent_volume pcr_cycles submit_for_sequencing sub_pool coverage sample_volume] + .freeze + + # Returns the contents of the header row for the well detail columns + def well_details_header_row + @well_details_header_row ||= PcrCyclesBinnedPlate::CsvFile::DuplexSeq::WellDetailsHeader.new(@data[2]) if @data[ + 2 + ] + end + + private + + def get_config_details_from_purpose(config) + Utility::PcrCyclesForDuplexSeqCsvFileUploadConfig.new(config) + end + + def create_row(index, row_data) + PcrCyclesBinnedPlate::CsvFile::DuplexSeq::Row.new(@config, well_details_header_row, index + 2, row_data) + end + + # Gates looking for wells if the file is invalid + def correctly_formatted? + correctly_parsed? && plate_barcode_header_row.valid? && well_details_header_row.valid? + end + end + end +end diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_t_nano_seq.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_t_nano_seq.rb new file mode 100644 index 000000000..264cb30a0 --- /dev/null +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_t_nano_seq.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require './lib/nested_validation' +require 'csv' + +# Part of the Labware creator classes +module LabwareCreators + require_dependency 'labware_creators/pcr_cycles_binned_plate_for_t_nano_seq' + + module PcrCyclesBinnedPlate + # + # This version of the csv file is for Targeted NanoSeq. + # + class CsvFileForTNanoSeq < CsvFileBase + delegate :hyb_panel_column, to: :well_details_header_row + + FIELDS_FOR_WELL_DETAILS = %w[ + concentration + input_amount_available + input_amount_desired + sample_volume + diluent_volume + pcr_cycles + hyb_panel + ].freeze + + # Returns the contents of the header row for the well detail columns + def well_details_header_row + @well_details_header_row ||= PcrCyclesBinnedPlate::CsvFile::TNanoSeq::WellDetailsHeader.new(@data[2]) if @data[ + 2 + ] + end + + private + + def get_config_details_from_purpose(config) + Utility::PcrCyclesForTNanoSeqCsvFileUploadConfig.new(config) + end + + def create_row(index, row_data) + PcrCyclesBinnedPlate::CsvFile::TNanoSeq::Row.new(@config, well_details_header_row, index + 2, row_data) + end + end + end +end diff --git a/app/models/labware_creators/pcr_cycles_binned_plate.rb b/app/models/labware_creators/pcr_cycles_binned_plate_base.rb similarity index 76% rename from app/models/labware_creators/pcr_cycles_binned_plate.rb rename to app/models/labware_creators/pcr_cycles_binned_plate_base.rb index d1ef9e1e4..1f471a518 100644 --- a/app/models/labware_creators/pcr_cycles_binned_plate.rb +++ b/app/models/labware_creators/pcr_cycles_binned_plate_base.rb @@ -3,12 +3,11 @@ module LabwareCreators # Handles the generation of a plate with wells binned according to the number of # PCR cycles that has been determined by the customer. - # Uploads a file supplied by the customer that has a row per each well and - # includes Sample Volume, Diluent Volume, PCR Cycles, Sub-Pool and Coverage columns. + # Uploads a file supplied by the customer that has a row per each well. # Uses the PCR Cycles column to determine the binning arrangement of the wells, # and the Sample Volume and Diluent Volume columns in the well transfers. - # Sub-Pool and Coverage need to be stored for a later step downstream in the pipeline, - # at the point where custom pooling is performed. + # Values from some columns need to be stored for a later file export step downstream + # in the pipeline. # Wells in the bins are applied to the destination by column order. # If there is enough space on the destination plate each new bin will start in a new # column. Otherwise bins will run consecutively without gaps. @@ -27,7 +26,7 @@ module LabwareCreators # |E1| pcr_cycles = 12 (bin 2) | | | | # +--+--+--~ +--+--+--~ # |G1| pcr_cycles = 12 (bin 2) | | | | - class PcrCyclesBinnedPlate < StampedPlate + class PcrCyclesBinnedPlateBase < StampedPlate include LabwareCreators::CustomPage MISSING_WELL_DETAIL = 'is missing a row for well %s, all wells with content must have a row in the uploaded file.' @@ -77,26 +76,7 @@ def save end def after_transfer! - # called as part of the 'super' call in the 'save' method - # retrieve child plate through v2 api, using uuid got through v1 api - child_v2 = Sequencescape::Api::V2.plate_with_custom_includes(CHILD_PLATE_INCLUDES, uuid: child.uuid) - - # update fields on each well with various metadata - fields_to_update = %w[diluent_volume pcr_cycles submit_for_sequencing sub_pool coverage] - - child_wells_by_location = child_v2.wells.index_by(&:location) - - well_details.each do |parent_location, details| - child_position = transfer_hash[parent_location]['dest_locn'] - child_well = child_wells_by_location[child_position] - - update_well_with_metadata(child_well, details, fields_to_update) - end - end - - def update_well_with_metadata(well, metadata, fields_to_update) - options = fields_to_update.index_with { |field| metadata[field] } - well.update(options) + raise '#after_transfer! must be implemented on subclasses' end def wells_have_required_information? @@ -123,12 +103,17 @@ def filtered_wells # Upload the csv file onto the plate via api v1 # def upload_file - parent_v1.qc_files.create_from_file!(file, 'duplex_seq_customer_file.csv') + parent_v1.qc_files.create_from_file!(file, customer_filename) + end + + # filename for the customer file upload + def customer_filename + raise '#csv_file must be implemented on subclasses' end # Create class that will parse and validate the uploaded file def csv_file - @csv_file ||= CsvFile.new(file, csv_file_upload_config, parent.human_barcode) + raise '#csv_file must be implemented on subclasses' end # Override this method in sub-class if required. diff --git a/app/models/labware_creators/pcr_cycles_binned_plate_for_duplex_seq.rb b/app/models/labware_creators/pcr_cycles_binned_plate_for_duplex_seq.rb new file mode 100644 index 000000000..382dd20ee --- /dev/null +++ b/app/models/labware_creators/pcr_cycles_binned_plate_for_duplex_seq.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module LabwareCreators + # Handles the generation of a plate with wells binned according to the number of + # PCR cycles that has been determined by the customer. + # Uploads a file supplied by the customer that has a row per each well and + # includes Sample Volume, Diluent Volume, PCR Cycles, Sub-Pool and Coverage columns. + # Uses the PCR Cycles column to determine the binning arrangement of the wells, + # and the Sample Volume and Diluent Volume columns in the well transfers. + # Sub-Pool and Coverage need to be stored for a later step downstream in the pipeline, + # at the point where custom pooling is performed. + # Wells in the bins are applied to the destination by column order. + # If there is enough space on the destination plate each new bin will start in a new + # column. Otherwise bins will run consecutively without gaps. + # + # + # Source Plate Dest Plate + # +--+--+--~ +--+--+--~ + # |A1| pcr_cycles = 12 (bin 2) |B1|A1|C1| + # +--+--+--~ +--+--+--~ + # |B1| pcr_cycles = 15 (bin 1) |D1|E1| | + # +--+--+--~ + +--+--+--~ + # |C1| pcr_cycles = 10 (bin 3) | |G1| | + # +--+--+--~ +--+--+--~ + # |D1| pcr_cycles = 15 (bin 1) | | | | + # +--+--+--~ +--+--+--~ + # |E1| pcr_cycles = 12 (bin 2) | | | | + # +--+--+--~ +--+--+--~ + # |G1| pcr_cycles = 12 (bin 2) | | | | + class PcrCyclesBinnedPlateForDuplexSeq < PcrCyclesBinnedPlateBase + self.page = 'pcr_cycles_binned_plate' + + CUSTOMER_FILENAME = 'duplex_seq_customer_file.csv' + + def after_transfer! + # called as part of the 'super' call in the 'save' method + # retrieve child plate through v2 api, using uuid got through v1 api + child_v2 = Sequencescape::Api::V2.plate_with_custom_includes(CHILD_PLATE_INCLUDES, uuid: child.uuid) + + # update fields on each well with various metadata + fields_to_update = %w[diluent_volume pcr_cycles submit_for_sequencing sub_pool coverage] + + child_wells_by_location = child_v2.wells.index_by(&:location) + + well_details.each do |parent_location, details| + child_position = transfer_hash[parent_location]['dest_locn'] + child_well = child_wells_by_location[child_position] + + update_well_with_metadata(child_well, details, fields_to_update) + end + end + + def update_well_with_metadata(well, metadata, fields_to_update) + options = fields_to_update.index_with { |field| metadata[field] } + well.update(options) + end + + private + + # filename for the customer file upload + def customer_filename + CUSTOMER_FILENAME + end + + # Create class that will parse and validate the uploaded file + def csv_file + @csv_file ||= PcrCyclesBinnedPlate::CsvFileForDuplexSeq.new(file, csv_file_upload_config, parent.human_barcode) + end + end +end diff --git a/app/models/labware_creators/pcr_cycles_binned_plate_for_t_nano_seq.rb b/app/models/labware_creators/pcr_cycles_binned_plate_for_t_nano_seq.rb new file mode 100644 index 000000000..ef8afc8b0 --- /dev/null +++ b/app/models/labware_creators/pcr_cycles_binned_plate_for_t_nano_seq.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module LabwareCreators + # This version of the class is specific to the Targeted NanoSeq pipeline. + class PcrCyclesBinnedPlateForTNanoSeq < PcrCyclesBinnedPlateBase + self.page = 'pcr_cycles_binned_plate_for_t_nano_seq' + + CUSTOMER_FILENAME = 'targeted_nano_seq_customer_file.csv' + + # rubocop:disable Metrics/AbcSize + def after_transfer! + # called as part of the 'super' call in the 'save' method + # retrieve child plate through v2 api, using uuid got through v1 api + child_v2_plate = Sequencescape::Api::V2.plate_with_custom_includes(CHILD_PLATE_INCLUDES, uuid: child.uuid) + + # update fields on each well with various metadata + child_wells_by_location = child_v2_plate.wells.index_by(&:location) + + well_details.each do |parent_location, details| + child_well_location = transfer_hash[parent_location]['dest_locn'] + child_well = child_wells_by_location[child_well_location] + + # NB. this seems to return an array of requests via the api but a single request in tests + request = Array(child_well.aliquots.first.request).first + + if request.blank? + raise StandardError, "Unable to identify request for child plate well at location #{child_well_location}" + end + + # create hash containing the key value pairs we want to store as metadata on the request + request_metadata = { + 'original_plate_barcode' => parent.human_barcode, + 'original_well_id' => parent_location, + 'concentration_nm' => details['concentration'], + 'input_amount_available' => details['input_amount_available'], + 'input_amount_desired' => details['input_amount_desired'], + 'sample_volume' => details['sample_volume'], + 'diluent_volume' => details['diluent_volume'], + 'pcr_cycles' => details['pcr_cycles'], + 'hyb_panel' => details['hyb_panel'] + } + create_request_metadata(request, request_metadata, child_well_location) + end + end + + # rubocop:enable Metrics/AbcSize + + # Cycles through a hash of key value pairs and creates a new metadatum for each on the request object. + # NB. makes assumption that metadata with same name does not already exist i.e. we create not update + def create_request_metadata(request, request_metadata, child_well_location) + request_metadata.each do |metadata_key, metadata_value| + pm_v2 = Sequencescape::Api::V2::PolyMetadatum.new(key: metadata_key, value: metadata_value) + + # NB. this is the only way to set the relationship between the polymetadatum and the request, after + # the polymetadatum object has been created + pm_v2.relationships.metadatable = request + + next if pm_v2.save + + raise StandardError, + "New metadata for request (key: #{metadata_key}, value: #{metadata_value}) " \ + "did not save for request at child well location #{child_well_location}" + end + end + + private + + # filename for the customer file upload + def customer_filename + CUSTOMER_FILENAME + end + + # Create class that will parse and validate the uploaded file + def csv_file + @csv_file ||= PcrCyclesBinnedPlate::CsvFileForTNanoSeq.new(file, csv_file_upload_config, parent.human_barcode) + end + end +end diff --git a/app/models/presenters/pcr_cycles_binned_plate_presenter.rb b/app/models/presenters/pcr_cycles_binned_plate_presenter_base.rb similarity index 56% rename from app/models/presenters/pcr_cycles_binned_plate_presenter.rb rename to app/models/presenters/pcr_cycles_binned_plate_presenter_base.rb index 08dd1bc74..56b581917 100644 --- a/app/models/presenters/pcr_cycles_binned_plate_presenter.rb +++ b/app/models/presenters/pcr_cycles_binned_plate_presenter_base.rb @@ -5,19 +5,22 @@ module Presenters # The PcrCyclesBinnedPlatePresenter is used for plates that have had # pcr cycle binning applied. It shows a view of the plate with colours # and keys indicating the various bins. + # This is the base class for the PcrCyclesBinnedPlatePresenter and should + # not be used directly. + # NB. Once DuplexSeq is converted to use the new request poly_metadata, this + # subclassing can be removed and the PcrCyclesBinnedPlateUsingRequestMetadataPresenter + # version will be the only version needed. # - class PcrCyclesBinnedPlatePresenter < PlatePresenter + class PcrCyclesBinnedPlatePresenterBase < PlatePresenter include Presenters::Statemachine::Standard - CURRENT_PLATE_INCLUDES = 'wells.aliquots,wells.qc_results' - self.summary_partial = 'labware/plates/binned_summary' self.aliquot_partial = 'binned_aliquot' validates_with Validators::ActiveRequestValidator def current_plate - @current_plate ||= Sequencescape::Api::V2.plate_with_custom_includes(CURRENT_PLATE_INCLUDES, uuid: labware.uuid) + @current_plate ||= Sequencescape::Api::V2.plate_with_custom_includes(current_plate_includes, uuid: labware.uuid) end def dilutions_calculator @@ -32,19 +35,14 @@ def bin_details @bin_details ||= dilutions_calculator.compute_presenter_bin_details end + def current_plate_includes + raise 'Method current_plate_includes must be implemented in a subclass of PcrCyclesBinnedPlatePresenterBase' + end + private def well_details - # For each well with aliquots on the plate select the pcr cycles metadata - # { 'A1' => { 'pcr_cycles' => 16 }, 'B1' => etc. } - @well_details ||= - current_plate - .wells - .each_with_object({}) do |well, details| - next if well.aliquots.empty? - - details[well.location] = { 'pcr_cycles' => well.attributes['pcr_cycles'] } - end + raise 'Method well_details must be implemented in a subclass of PcrCyclesBinnedPlatePresenterBase' end end end diff --git a/app/models/presenters/pcr_cycles_binned_plate_using_request_metadata_presenter.rb b/app/models/presenters/pcr_cycles_binned_plate_using_request_metadata_presenter.rb new file mode 100644 index 000000000..bd56352ed --- /dev/null +++ b/app/models/presenters/pcr_cycles_binned_plate_using_request_metadata_presenter.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Presenters + # + # This version of the PcrCyclesBinnedPlatePresenter fetches metadata from + # the Request poly_metadata. + # + class PcrCyclesBinnedPlateUsingRequestMetadataPresenter < PcrCyclesBinnedPlatePresenterBase + # include Presenters::Statemachine::Standard + + CURRENT_PLATE_INCLUDES = 'wells.aliquots,wells.qc_results,wells.aliquots.request.poly_metadata' + + def current_plate_includes + CURRENT_PLATE_INCLUDES + end + + private + + # This version of well details fetches the pcr cycles from the request poly_metadata + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity + def well_details + # For each well with aliquots on the plate select the pcr cycles metadata + # { 'A1' => { 'pcr_cycles' => 16 }, 'B1' => etc. } + @well_details ||= + current_plate + .wells + .each_with_object({}) do |well, details| + next if well.aliquots.empty? + + # Should be a value by this point in order to have calculated the binning + # NB. poly_metadata are stored as strings so need to convert to integer + pcr_cycles = + well.aliquots.first.request.poly_metadata.find { |md| md.key == 'pcr_cycles' }&.value.to_i || nil + raise "No pcr_cycles metadata found for well #{well.location}" if pcr_cycles.nil? + + details[well.location] = { 'pcr_cycles' => pcr_cycles } + end + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity + end +end diff --git a/app/models/presenters/pcr_cycles_binned_plate_using_well_metadata_presenter.rb b/app/models/presenters/pcr_cycles_binned_plate_using_well_metadata_presenter.rb new file mode 100644 index 000000000..50a702ef0 --- /dev/null +++ b/app/models/presenters/pcr_cycles_binned_plate_using_well_metadata_presenter.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Presenters + # + # This version of the PcrCyclesBinnedPlatePresenter fetches metadata from + # the wells. + # + class PcrCyclesBinnedPlateUsingWellMetadataPresenter < PcrCyclesBinnedPlatePresenterBase + CURRENT_PLATE_INCLUDES = 'wells.aliquots,wells.qc_results' + + def current_plate_includes + CURRENT_PLATE_INCLUDES + end + + private + + # This version of well details fetches the pcr cycles from the well metadata + def well_details + # For each well with aliquots on the plate select the pcr cycles metadata + # { 'A1' => { 'pcr_cycles' => 16 }, 'B1' => etc. } + @well_details ||= + current_plate + .wells + .each_with_object({}) do |well, details| + next if well.aliquots.empty? + + # Should be a value by this point in order to have calculated the binning + pcr_cycles = well.attributes['pcr_cycles'] || nil + raise "No pcr_cycles value found on well #{well.location}" if pcr_cycles.nil? + + details[well.location] = { 'pcr_cycles' => pcr_cycles } + end + end + end +end diff --git a/app/models/utility/pcr_cycles_csv_file_upload_config.rb b/app/models/utility/pcr_cycles_csv_file_upload_config_base.rb similarity index 77% rename from app/models/utility/pcr_cycles_csv_file_upload_config.rb rename to app/models/utility/pcr_cycles_csv_file_upload_config_base.rb index 864d87ce1..cbd55caa5 100644 --- a/app/models/utility/pcr_cycles_csv_file_upload_config.rb +++ b/app/models/utility/pcr_cycles_csv_file_upload_config_base.rb @@ -2,7 +2,7 @@ module Utility # Handles the extraction of dilution configuration functions for pcr cycle binning. - class PcrCyclesCsvFileUploadConfig + class PcrCyclesCsvFileUploadConfigBase include ActiveModel::Model attr_reader :csv_file_config @@ -15,14 +15,18 @@ class PcrCyclesCsvFileUploadConfig diluent_volume_min: 'to_f', diluent_volume_max: 'to_f', pcr_cycles_min: 'to_i', - pcr_cycles_max: 'to_i', - sub_pool_min: 'to_i', - sub_pool_max: 'to_i' + pcr_cycles_max: 'to_i' }.freeze def initialize(csv_file_config) @csv_file_config = csv_file_config CONFIG_VARIABLES.each { |k, v| create_method(k) { @csv_file_config[k].send(v) } } + + initialize_pipeline_specific_methods + end + + def initialize_pipeline_specific_methods + raise '#initialize_pipeline_specific_methods must be implemented on subclasses' end def create_method(name, &block) diff --git a/app/models/utility/pcr_cycles_for_duplex_seq_csv_file_upload_config.rb b/app/models/utility/pcr_cycles_for_duplex_seq_csv_file_upload_config.rb new file mode 100644 index 000000000..8f9241c4c --- /dev/null +++ b/app/models/utility/pcr_cycles_for_duplex_seq_csv_file_upload_config.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Utility + # This version is for the Duplex Seq pipeline. + class PcrCyclesForDuplexSeqCsvFileUploadConfig < PcrCyclesCsvFileUploadConfigBase + PIPELINE_SPECIFIC_CONFIG_VARIABLES = { sub_pool_min: 'to_i', sub_pool_max: 'to_i' }.freeze + + def initialize_pipeline_specific_methods + PIPELINE_SPECIFIC_CONFIG_VARIABLES.each { |k, v| create_method(k) { @csv_file_config[k].send(v) } } + end + + def submit_for_sequencing_valid_values + @csv_file_config.fetch(:submit_for_sequencing_valid_values, []).map + end + end +end diff --git a/app/models/utility/pcr_cycles_for_t_nano_seq_csv_file_upload_config.rb b/app/models/utility/pcr_cycles_for_t_nano_seq_csv_file_upload_config.rb new file mode 100644 index 000000000..e6baafbf4 --- /dev/null +++ b/app/models/utility/pcr_cycles_for_t_nano_seq_csv_file_upload_config.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Utility + # This version is for the Targeted NanoSeq pipeline. + class PcrCyclesForTNanoSeqCsvFileUploadConfig < PcrCyclesCsvFileUploadConfigBase + PIPELINE_SPECIFIC_CONFIG_VARIABLES = {}.freeze + + def initialize_pipeline_specific_methods + PIPELINE_SPECIFIC_CONFIG_VARIABLES.each { |k, v| create_method(k) { @csv_file_config[k].send(v) } } + end + end +end diff --git a/app/views/exports/targeted_nanoseq_al_lib_concentrations_for_customer.csv.erb b/app/views/exports/targeted_nanoseq_al_lib_concentrations_for_customer.csv.erb index 93fb1da25..98339ceb8 100644 --- a/app/views/exports/targeted_nanoseq_al_lib_concentrations_for_customer.csv.erb +++ b/app/views/exports/targeted_nanoseq_al_lib_concentrations_for_customer.csv.erb @@ -1,8 +1,8 @@ <%= CSV.generate_line ['Plate Barcode', @plate.labware_barcode.human], row_sep: "" %> -<%= CSV.generate_line ['Well', 'Concentration (nM)', 'Sanger Sample Id', 'Supplier Sample Name', 'Input amount available (fmol)', 'Input amount desired', 'Sample volume', 'Diluent volume', 'PCR cycles', 'Submit for sequencing (Y/N)?', 'Sub-Pool', 'Coverage'], row_sep: "" %> +<%= CSV.generate_line ['Well', 'Concentration (nM)', 'Sanger Sample Id', 'Supplier Sample Name', 'Input amount available (fmol)', 'Input amount desired', 'Sample volume', 'Diluent volume', 'Hyb Panel'], row_sep: "" %> <% @plate.wells_in_columns.each do |well| %> <% unless well.empty? %> -<%= CSV.generate_line [well.location, well.latest_molarity&.value, well.sanger_sample_id, well.supplier_name, well.input_amount_available, nil, nil, nil, nil, nil, nil, nil], row_sep: "" %> +<%= CSV.generate_line [well.location, well.latest_molarity&.value, well.sanger_sample_id, well.supplier_name, well.input_amount_available, nil, nil, nil, nil], row_sep: "" %> <% end %> <% end %> diff --git a/app/views/exports/targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling.csv.erb b/app/views/exports/targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling.csv.erb deleted file mode 100644 index f320a786a..000000000 --- a/app/views/exports/targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling.csv.erb +++ /dev/null @@ -1,11 +0,0 @@ -<%= CSV.generate_line ['Well', 'Concentration (ng/ul)', 'Submit for sequencing (Y/N)?', 'Sub-Pool', 'Coverage'], row_sep: "" %> -<% @plate.wells_in_columns.each_with_index do |well, well_index| %> - <% unless well.empty? || @ancestor_plate.blank? %> - <% ancestor_well = @ancestor_plate.wells_in_columns[well_index] %> - <% if ancestor_well.attributes['submit_for_sequencing'] %> -<%= CSV.generate_line [well.location, well.latest_concentration&.value, 'Y', ancestor_well.attributes['sub_pool']&.to_i, ancestor_well.attributes['coverage']&.to_i], row_sep: "" %> - <% else %> -<%= CSV.generate_line [well.location, well.latest_concentration&.value, 'N', nil, nil], row_sep: "" %> - <% end %> - <% end %> -<% end %> \ No newline at end of file diff --git a/app/views/exports/targeted_nanoseq_pcr_xp_merged_file.csv.erb b/app/views/exports/targeted_nanoseq_pcr_xp_merged_file.csv.erb new file mode 100644 index 000000000..1cb149295 --- /dev/null +++ b/app/views/exports/targeted_nanoseq_pcr_xp_merged_file.csv.erb @@ -0,0 +1,22 @@ +<%= CSV.generate_line ['Original Plate Barcode', 'Original Well ID', 'Concentration (nM)', 'Sanger Sample ID', 'Supplier Sample Name', 'Input amount available (fmol)', 'Input amount desired (fmol)', 'New Plate Barcode', 'New Well ID', 'Concentration (ng/ul)', 'Hyb Panel'], row_sep: "" %> +<% row_array = [] %> +<% @plate.wells_in_columns.each_with_index do |well, well_index| %> + <% unless well.empty? %> + <% request = Array(well.aliquots.first.request).first %> + <% sample = well.aliquots.first.sample %> + <% sample_id = sample.sanger_sample_id %> + <% supplier_sample_name = sample.sample_metadata.supplier_name %> + <% md_original_plate_barcode = request.poly_metadata.select { |md| md.key == 'original_plate_barcode' }.first&.value || nil %> + <% md_original_well_id = request.poly_metadata.select { |md| md.key == 'original_well_id' }.first&.value || nil %> + <% md_concentration_nm = request.poly_metadata.select { |md| md.key == 'concentration_nm' }.first&.value || nil %> + <% md_input_amount_available = request.poly_metadata.select { |md| md.key == 'input_amount_available' }.first&.value || nil %> + <% md_input_amount_desired = request.poly_metadata.select { |md| md.key == 'input_amount_desired' }.first&.value || nil %> + <% md_hyb_panel = request.poly_metadata.select { |md| md.key == 'hyb_panel' }.first&.value || nil %> + <% row_array.push([md_original_plate_barcode, md_original_well_id, md_concentration_nm, sample_id, supplier_sample_name, md_input_amount_available, md_input_amount_desired, @plate.human_barcode, well.location, well.latest_concentration&.value, md_hyb_panel]) %> + <% end %> +<% end %> +<% column_order = (1..12).to_a.product(('A'..'H').to_a).map(&:reverse).map(&:join) %> +<% sorted_row_array = row_array.sort_by! { |row| [row[0], column_order.index(row[1])] } %> +<% sorted_row_array.each do |row| %> +<%= CSV.generate_line row, row_sep: "" %> +<% end %> diff --git a/app/views/plate_creation/pcr_cycles_binned_plate_for_t_nano_seq.html.erb b/app/views/plate_creation/pcr_cycles_binned_plate_for_t_nano_seq.html.erb new file mode 100644 index 000000000..02d5bff33 --- /dev/null +++ b/app/views/plate_creation/pcr_cycles_binned_plate_for_t_nano_seq.html.erb @@ -0,0 +1,62 @@ +<%= page(:'pcr-cycles-binned-plate-for-t-nano-seq') do -%> + <%= content do %> + <%= card title: 'Help' do %> +

Upload the customer completed csv file describing your desired pcr cycles binning strategy. An example is shown below:

+

Please make sure there is a header row for the parent plate barcode, this barcode must match the plate from which the dilution plate is being created. Then leave a spacer row before the row column headings. +

Please also make sure you specify source and diluent volumes, number of pcr cycles, and the hyb panel to use for each well that has a sample.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Plate barcodeDN12345678A
WellConcentration (nM)Sanger Sample IdSupplier Sample NameInput amount available (fmol)Input amount desiredSample volumeDiluent volumePCR cyclesHyb Panel
A115.2101Smp 145.350.05.020.012My Hyb Panel Name
B10.12102Smp 27.150.025.00.016My Hyb Panel Name
C18.7103Smp 330.450.010.015.012My Hyb Panel Name
D121.3104Smp 425.050.04.218.812My Hyb Panel Name
E111.8105Smp 516.950.08.716.314My Hyb Panel Name
F19.1106Smp 636.250.09.617.114My Hyb Panel Name
G10.03107Smp 719.750.025.00.012My Hyb Panel Name
H11.8108Smp 816.650.023.51.514My Hyb Panel Name
A27.6109Smp 942.250.012.612.416My Hyb Panel Name
B214.2110Smp 1029.550.09.515.412My Hyb Panel Name
+

In this example we will generate 3 bins:

+ +

NB. All wells with aliquots in the parent must have values.

+ <% end %> + <% end %> + <%= sidebar do %> + <%= card title: 'File upload' do %> + <%= form_for(@labware_creator, as: :plate, url: limber_plate_children_path(@labware_creator.parent)) do |f| %> + <%= f.hidden_field :purpose_uuid %> +
+ <%= f.file_field :file, accept: '.csv', required: true %> +
+ <%= f.submit class: 'btn btn-success' %> + <% end %> + <% end %> + <% end %> +<%- end -%> diff --git a/config/exports/exports.yml b/config/exports/exports.yml index 6c41c6c9c..0859f7938 100644 --- a/config/exports/exports.yml +++ b/config/exports/exports.yml @@ -15,9 +15,9 @@ duplex_seq_pcr_xp_concentrations_for_custom_pooling: targeted_nanoseq_al_lib_concentrations_for_customer: csv: targeted_nanoseq_al_lib_concentrations_for_customer plate_includes: wells.qc_results,wells.aliquots.sample.sample_metadata -targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling: - csv: targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling - plate_includes: wells.qc_results +targeted_nanoseq_pcr_xp_merged_file: + csv: targeted_nanoseq_pcr_xp_merged_file + plate_includes: wells.qc_results,wells.aliquots.sample.sample_metadata,wells.aliquots.request.poly_metadata ancestor_purpose: LTN AL Lib Dil hamilton_aggregate_cherrypick: csv: hamilton_aggregate_cherrypick diff --git a/config/purposes/duplex_seq.yml b/config/purposes/duplex_seq.yml index e5e88ed87..c6e0013a3 100644 --- a/config/purposes/duplex_seq.yml +++ b/config/purposes/duplex_seq.yml @@ -25,8 +25,8 @@ LDS AL Lib: id: 'duplex_seq_al_lib_concentrations_for_customer' LDS AL Lib Dil: :asset_type: plate - :presenter_class: Presenters::PcrCyclesBinnedPlatePresenter - :creator_class: LabwareCreators::PcrCyclesBinnedPlate + :presenter_class: Presenters::PcrCyclesBinnedPlateUsingWellMetadataPresenter + :creator_class: LabwareCreators::PcrCyclesBinnedPlateForDuplexSeq :csv_file_upload: :input_amount_desired_min: 0.0 :input_amount_desired_max: 10000.0 diff --git a/config/purposes/targeted_nanoseq.yml b/config/purposes/targeted_nanoseq.yml index 6a6d66464..c34edb026 100644 --- a/config/purposes/targeted_nanoseq.yml +++ b/config/purposes/targeted_nanoseq.yml @@ -29,8 +29,8 @@ LTN AL Lib: id: 'targeted_nanoseq_al_lib_concentrations_for_customer' LTN AL Lib Dil: :asset_type: plate - :presenter_class: Presenters::PcrCyclesBinnedPlatePresenter - :creator_class: LabwareCreators::PcrCyclesBinnedPlate + :presenter_class: Presenters::PcrCyclesBinnedPlateUsingRequestMetadataPresenter + :creator_class: LabwareCreators::PcrCyclesBinnedPlateForTNanoSeq :csv_file_upload: :input_amount_desired_min: 0.0 :input_amount_desired_max: 10000.0 @@ -40,11 +40,6 @@ LTN AL Lib Dil: :diluent_volume_max: 50.0 :pcr_cycles_min: 1 :pcr_cycles_max: 20 - :submit_for_sequencing_valid_values: - - 'Y' - - 'N' - :sub_pool_min: 1 - :sub_pool_max: 96 :file_links: - name: 'Download Hamilton AL Lib to Dilution CSV' id: 'hamilton_ltn_al_lib_to_ltn_al_lib_dil' @@ -65,8 +60,8 @@ LTN Lib PCR XP: :default_printer_type: :plate_b :label_template: plate_xp :file_links: - - name: 'Download Concentration (ng/ul) CSV for Custom Pooling' - id: 'targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling' + - name: 'Download Merged File CSV' + id: 'targeted_nanoseq_pcr_xp_merged_file' LTN Custom Pool: :asset_type: tube :target: StockMultiplexedLibraryTube diff --git a/docs/presenters.md b/docs/presenters.md index f957f06a7..887127374 100644 --- a/docs/presenters.md +++ b/docs/presenters.md @@ -54,14 +54,13 @@ LBC 5p GEX Dil {Presenters::NormalisedBinnedPlatePresenter View class documentation} -### Presenters::PcrCyclesBinnedPlatePresenter +### Presenters::PcrCyclesBinnedPlatePresenterBase -{include:Presenters::PcrCyclesBinnedPlatePresenter} +{include:Presenters::PcrCyclesBinnedPlatePresenterBase} -Used directly in 2 purposes: -LDS AL Lib Dil and LTN AL Lib Dil +**This presenter is unused** -{Presenters::PcrCyclesBinnedPlatePresenter View class documentation} +{Presenters::PcrCyclesBinnedPlatePresenterBase View class documentation} ### Presenters::StandardPresenter diff --git a/spec/controllers/exports_controller_spec.rb b/spec/controllers/exports_controller_spec.rb index 47e60416e..9742c21fe 100644 --- a/spec/controllers/exports_controller_spec.rb +++ b/spec/controllers/exports_controller_spec.rb @@ -7,6 +7,9 @@ let(:default_plate_includes) { 'wells' } let(:well_qc_includes) { 'wells.qc_results' } let(:well_qc_sample_includes) { 'wells.qc_results,wells.aliquots.sample.sample_metadata' } + let(:well_with_request_metadata_includes) do + 'wells.qc_results,wells.aliquots.sample.sample_metadata,wells.aliquots.request.poly_metadata' + end let(:well_src_asset_includes) { 'wells.transfer_requests_as_target.source_asset' } let(:plate) { create :v2_plate, barcode_number: 1 } let(:plate_barcode) { 'DN1S' } @@ -99,10 +102,10 @@ it_behaves_like 'a csv view' end - context 'where csv id requested is targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling.csv' do - let(:includes) { well_qc_includes } - let(:csv_id) { 'targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling' } - let(:expected_template) { 'targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling' } + context 'where csv id requested is targeted_nanoseq_pcr_xp_merged_file.csv' do + let(:includes) { well_with_request_metadata_includes } + let(:csv_id) { 'targeted_nanoseq_pcr_xp_merged_file' } + let(:expected_template) { 'targeted_nanoseq_pcr_xp_merged_file' } it_behaves_like 'a csv view' end diff --git a/spec/factories/purpose_config_factories.rb b/spec/factories/purpose_config_factories.rb index 64d09028f..632ab4e3c 100644 --- a/spec/factories/purpose_config_factories.rb +++ b/spec/factories/purpose_config_factories.rb @@ -152,6 +152,21 @@ end end + factory :targeted_nano_seq_customer_csv_file_upload_purpose_config do + csv_file_upload do + { + input_amount_desired_min: 0.0, + input_amount_desired_max: 50.0, + sample_volume_min: 0.2, + sample_volume_max: 50.0, + diluent_volume_min: 0.0, + diluent_volume_max: 50.0, + pcr_cycles_min: 1, + pcr_cycles_max: 20 + } + end + end + # Configuration for an aggregation plate factory :aggregation_purpose_config do state_changer_class { 'StateChangers::AutomaticPlateStateChanger' } diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb index 942f424e6..41858ad04 100644 --- a/spec/factories/request_factories.rb +++ b/spec/factories/request_factories.rb @@ -81,9 +81,24 @@ end factory :library_request_with_poly_metadata do + # To use this factory create each poly_metadatum individually using the poly_metadatum factory, but don't + # set the metadatable relationship to the request. Then pass them in as an array to this request factory + # as an array of one or more poly_metadata and it sets the relationship here. transient { poly_metadata { [] } } - after(:build) { |request, evaluator| request.poly_metadata = evaluator.poly_metadata } + after(:build) do |request, evaluator| + # initialise the poly_metadata array + request.poly_metadata = [] + + # add each polymetadatum to the request + evaluator.poly_metadata.each do |pm| + # set the relationship between the polymetadatum and the request + pm.relationships.metadatable = request + + # link the polymetadatum to the request + request.poly_metadata.push(pm) + end + end end end diff --git a/spec/fixtures/config/exports/exports.yml b/spec/fixtures/config/exports/exports.yml index 9465c99f3..0d385cfe4 100644 --- a/spec/fixtures/config/exports/exports.yml +++ b/spec/fixtures/config/exports/exports.yml @@ -15,9 +15,9 @@ duplex_seq_pcr_xp_concentrations_for_custom_pooling: targeted_nanoseq_al_lib_concentrations_for_customer: csv: targeted_nanoseq_al_lib_concentrations_for_customer plate_includes: wells.qc_results,wells.aliquots.sample.sample_metadata -targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling: - csv: targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling - plate_includes: wells.qc_results +targeted_nanoseq_pcr_xp_merged_file: + csv: targeted_nanoseq_pcr_xp_merged_file + plate_includes: wells.qc_results,wells.aliquots.sample.sample_metadata,wells.aliquots.request.poly_metadata ancestor_purpose: LTN AL Lib Dil hamilton_aggregate_cherrypick: csv: hamilton_aggregate_cherrypick diff --git a/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file.csv b/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file.csv new file mode 100644 index 000000000..d75deef0d --- /dev/null +++ b/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file.csv @@ -0,0 +1,18 @@ +Plate Barcode,DN2T + +Well,Concentration (nM),Sanger Sample Id,Supplier Sample Name,Input amount available (fmol),Input amount desired,Sample volume,Diluent volume,PCR cycles,Hyb Panel +A1,0.686,1STDY1,test_1,17.150000000000002,0.0,5.0,25.0,14,My Panel +B1,0.623,1STDY2,test_2,15.575,50.0,5.0,25.0,14,My Panel +C1,,,,,,,1STDY, +D1,1.874,1STDY3,test_3,46.85,49.9,5.0,25.0,16,My Panel +E1,1.929,1STDY4,test_4,48.225,0.1,5.0,25.0,12,My Panel +F1,1.700,1STDY5,test_5,42.5,50.0,4.0,26.0,12,My Panel +H1,1.838,1STDY6,test_6,45.95,37.3,5.0,25.0,12,My Panel +A2,1.581,1STDY7,test_7,39.525,50.0,3.2,26.8.0,12,My Panel +B2,1.538,1STDY8,test_8,38.45,34.8,5.0,25.0,12,My Panel +C2,1.560,1STDY9,test_9,39.0,50.0,5.0,25.0,12,My Panel +D2,1.479,1STDY10,test_10,36.975,50.0,5.0,25.0,12,My Panel +E2,0.734,1STDY11,test_11,18.35,50.0,5.0,25.0,14,My Panel +F2,0.000,1STDY12,test_12,0.0,39.2,30.0,0.0,16,My Panel +G2,0.741,1STDY13,test_13,18.525,50.0,5.0,25.0,14,My Panel +H2,0.196,1STDY14,test_14,4.9,50.0,3.621,27.353,16,My Panel diff --git a/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_bom.csv b/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_bom.csv new file mode 100644 index 000000000..fb0aa3f54 --- /dev/null +++ b/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_bom.csv @@ -0,0 +1,18 @@ +Plate Barcode,DN2T + +Well,Concentration (nM),Sanger Sample Id,Supplier Sample Name,Input amount available (fmol),Input amount desired,Sample volume,Diluent volume,PCR cycles,Hyb Panel +A1,0.686,1STDY1,test_1,17.150000000000002,0.0,5.0,25.0,14,My Panel +B1,0.623,1STDY2,test_2,15.575,50.0,5.0,25.0,14,My Panel +C1,,,,,,,1STDY, +D1,1.874,1STDY3,test_3,46.85,49.9,5.0,25.0,16,My Panel +E1,1.929,1STDY4,test_4,48.225,0.1,5.0,25.0,12,My Panel +F1,1.700,1STDY5,test_5,42.5,50.0,4.0,26.0,12,My Panel +H1,1.838,1STDY6,test_6,45.95,37.3,5.0,25.0,12,My Panel +A2,1.581,1STDY7,test_7,39.525,50.0,3.2,26.8.0,12,My Panel +B2,1.538,1STDY8,test_8,38.45,34.8,5.0,25.0,12,My Panel +C2,1.560,1STDY9,test_9,39.0,50.0,5.0,25.0,12,My Panel +D2,1.479,1STDY10,test_10,36.975,50.0,5.0,25.0,12,My Panel +E2,0.734,1STDY11,test_11,18.35,50.0,5.0,25.0,14,My Panel +F2,0.000,1STDY12,test_12,0.0,39.2,30.0,0.0,16,My Panel +G2,0.741,1STDY13,test_13,18.525,50.0,5.0,25.0,14,My Panel +H2,0.196,1STDY14,test_14,4.9,50.0,3.621,27.353,16,My Panel diff --git a/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_invalid_wells.csv b/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_invalid_wells.csv new file mode 100644 index 000000000..6010c6025 --- /dev/null +++ b/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_invalid_wells.csv @@ -0,0 +1,20 @@ +Plate Barcode,DN2T + +Well,Concentration (nM),Sanger Sample Id,Supplier Sample Name,Input amount available (fmol),Input amount desired,Sample volume,Diluent volume,PCR cycles,Hyb Panel +A1,0.686,1STDY1,test_1,17.150000000000002,0.0,5.0,25.0,14,My Panel +B1,0.623,1STDY2,test_2,15.575,50.0,5.0,25.0,14,My Panel +C1,,,,,,,, +D1,1.874,1STDY3,test_3,46.85,49.9,5.0,25.0,12,My Panel +E1,1.929,1STDY4,test_4,48.225,0.1,5.0,25.0,12,My Panel +F1,1.700,1STDY5,test_5,42.5,50.0,4.0,26.0,12,My Panel +H1,1.838,1STDY6,test_6,45.95,37.3,5.0,25.0,12,My Panel +I1,1.838,1STDY6,test_6,45.95,37.3,5.0,25.0,12,My Panel +A2,1.581,1STDY7,test_7,39.525,50.0,3.2,26.8.0,12,My Panel +B2,1.538,1STDY8,test_8,38.45,34.8,5.0,25.0,12,My Panel +C2,1.560,1STDY9,test_9,39.0,50.0,5.0,25.0,12,My Panel +D2,1.479,1STDY10,test_10,36.975,50.0,5.0,25.0,12,My Panel +E2,0.734,1STDY11,test_11,18.35,50.0,5.0,25.0,14,My Panel +F2,0.000,1STDY12,test_12,0.0,39.2,30.0,0.0,16,My Panel +G2,0.741,1STDY13,test_13,18.525,50.0,5.0,25.0,14,My Panel +H2,0.196,1STDY14,test_14,4.9,50.0,3.621,27.353,16,My Panel + diff --git a/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_missing_values.csv b/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_missing_values.csv new file mode 100644 index 000000000..fe0c39d68 --- /dev/null +++ b/spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_missing_values.csv @@ -0,0 +1,18 @@ +Plate Barcode,DN2T + +Well,Concentration (nM),Sanger Sample Id,Supplier Sample Name,Input amount available (fmol),Input amount desired,Sample volume,Diluent volume,PCR cycles,Hyb Panel +A1,0.686,1STDY1,test_1,17.150000000000002,,5.0,25.0,14,My Panel +B1,0.623,1STDY2,test_2,15.575,50.0,,25.0,14,My Panel +C1,,,,,,,, +D1,1.874,1STDY3,test_3,46.85,49.9,5.0,,12,My Panel +E1,1.929,1STDY4,test_4,48.225,0.1,5.0,25.0,,My Panel +F1,1.700,1STDY5,test_5,42.5,50.0,4.0,26.0,12,My Panel +H1,1.838,1STDY6,test_6,45.95,37.3,5.0,25.0,12,My Panel +A2,1.581,1STDY7,test_7,39.525,50.0,3.2,26.8.0,12, +B2,1.538,1STDY8,test_8,38.45,34.8,5.0,25.0,12,My Panel +C2,1.560,1STDY9,test_9,39.0,50.0,5.0,25.0,12,My Panel +D2,1.479,1STDY10,test_10,36.975,50.0,5.0,25.0,12,My Panel +E2,0.734,1STDY11,test_11,18.35,50.0,5.0,25.0,14,My Panel +F2,0.000,1STDY12,test_12,0.0,39.2,30.0,0.0,16,My Panel +G2,0.741,1STDY13,test_13,18.525,50.0,5.0,25.0,14,My Panel +H2,0.196,1STDY14,test_14,4.9,50.0,3.621,27.353,16,My Panel diff --git a/spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_spec.rb b/spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq_spec.rb similarity index 98% rename from spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_spec.rb rename to spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq_spec.rb index 05bce229c..47b61f1f6 100644 --- a/spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_spec.rb +++ b/spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe LabwareCreators::PcrCyclesBinnedPlate::CsvFile, with: :uploader do +RSpec.describe LabwareCreators::PcrCyclesBinnedPlate::CsvFileForDuplexSeq, with: :uploader do let(:purpose_config) { create :duplex_seq_customer_csv_file_upload_purpose_config } let(:csv_file_config) { purpose_config.fetch(:csv_file_upload) } diff --git a/spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_t_nano_seq_spec.rb b/spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_t_nano_seq_spec.rb new file mode 100644 index 000000000..a7cf2dead --- /dev/null +++ b/spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_t_nano_seq_spec.rb @@ -0,0 +1,313 @@ +# frozen_string_literal: true + +RSpec.describe LabwareCreators::PcrCyclesBinnedPlate::CsvFileForTNanoSeq, with: :uploader do + let(:purpose_config) { create :targeted_nano_seq_customer_csv_file_upload_purpose_config } + let(:csv_file_config) { purpose_config.fetch(:csv_file_upload) } + + subject { described_class.new(file, csv_file_config, 'DN2T') } + + context 'Valid files' do + let(:expected_well_details) do + { + 'A1' => { + 'concentration' => 0.686, + 'input_amount_available' => 17.150000000000002, + 'input_amount_desired' => 0.0, + 'sample_volume' => 5.0, + 'diluent_volume' => 25.0, + 'pcr_cycles' => 14, + 'hyb_panel' => 'My Panel' + }, + 'B1' => { + 'concentration' => 0.623, + 'input_amount_available' => 15.575, + 'input_amount_desired' => 50.0, + 'sample_volume' => 5.0, + 'diluent_volume' => 25.0, + 'pcr_cycles' => 14, + 'hyb_panel' => 'My Panel' + }, + 'D1' => { + 'concentration' => 1.874, + 'input_amount_available' => 46.85, + 'input_amount_desired' => 49.9, + 'sample_volume' => 5.0, + 'diluent_volume' => 25.0, + 'pcr_cycles' => 16, + 'hyb_panel' => 'My Panel' + }, + 'E1' => { + 'concentration' => 1.929, + 'input_amount_available' => 48.225, + 'input_amount_desired' => 0.1, + 'sample_volume' => 5.0, + 'diluent_volume' => 25.0, + 'pcr_cycles' => 12, + 'hyb_panel' => 'My Panel' + }, + 'F1' => { + 'concentration' => 1.700, + 'input_amount_available' => 42.5, + 'input_amount_desired' => 50.0, + 'sample_volume' => 4.0, + 'diluent_volume' => 26.0, + 'pcr_cycles' => 12, + 'hyb_panel' => 'My Panel' + }, + 'H1' => { + 'concentration' => 1.838, + 'input_amount_available' => 45.95, + 'input_amount_desired' => 37.3, + 'sample_volume' => 5.0, + 'diluent_volume' => 25.0, + 'pcr_cycles' => 12, + 'hyb_panel' => 'My Panel' + }, + 'A2' => { + 'concentration' => 1.581, + 'input_amount_available' => 39.525, + 'input_amount_desired' => 50.0, + 'sample_volume' => 3.2, + 'diluent_volume' => 26.8, + 'pcr_cycles' => 12, + 'hyb_panel' => 'My Panel' + }, + 'B2' => { + 'concentration' => 1.538, + 'input_amount_available' => 38.45, + 'input_amount_desired' => 34.8, + 'sample_volume' => 5.0, + 'diluent_volume' => 25.0, + 'pcr_cycles' => 12, + 'hyb_panel' => 'My Panel' + }, + 'C2' => { + 'concentration' => 1.560, + 'input_amount_available' => 39.0, + 'input_amount_desired' => 50.0, + 'sample_volume' => 5.0, + 'diluent_volume' => 25.0, + 'pcr_cycles' => 12, + 'hyb_panel' => 'My Panel' + }, + 'D2' => { + 'concentration' => 1.479, + 'input_amount_available' => 36.975, + 'input_amount_desired' => 50.0, + 'sample_volume' => 5.0, + 'diluent_volume' => 25.0, + 'pcr_cycles' => 12, + 'hyb_panel' => 'My Panel' + }, + 'E2' => { + 'concentration' => 0.734, + 'input_amount_available' => 18.35, + 'input_amount_desired' => 50.0, + 'sample_volume' => 5.0, + 'diluent_volume' => 25.0, + 'pcr_cycles' => 14, + 'hyb_panel' => 'My Panel' + }, + 'F2' => { + 'concentration' => 0.000, + 'input_amount_available' => 0.0, + 'input_amount_desired' => 39.2, + 'sample_volume' => 30.0, + 'diluent_volume' => 0.0, + 'pcr_cycles' => 16, + 'hyb_panel' => 'My Panel' + }, + 'G2' => { + 'concentration' => 0.741, + 'input_amount_available' => 18.525, + 'input_amount_desired' => 50.0, + 'sample_volume' => 5.0, + 'diluent_volume' => 25.0, + 'pcr_cycles' => 14, + 'hyb_panel' => 'My Panel' + }, + 'H2' => { + 'concentration' => 0.196, + 'input_amount_available' => 4.9, + 'input_amount_desired' => 50.0, + 'sample_volume' => 3.621, + 'diluent_volume' => 27.353, + 'pcr_cycles' => 16, + 'hyb_panel' => 'My Panel' + } + } + end + + context 'Without byte order markers' do + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file.csv', + 'sequencescape/qc_file' + ) + end + + describe '#valid?' do + it 'should be valid' do + expect(subject.valid?).to be true + end + end + + describe '#well_details' do + it 'should parse the expected well details' do + expect(subject.well_details).to eq expected_well_details + end + end + end + + context 'With byte order markers' do + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_bom.csv', + 'sequencescape/qc_file' + ) + end + + describe '#valid?' do + it 'should be valid' do + expect(subject.valid?).to be true + end + end + + describe '#well_details' do + it 'should parse the expected well details' do + expect(subject.well_details).to eq expected_well_details + end + end + end + end + + context 'something that can not parse' do + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file.csv', + 'sequencescape/qc_file' + ) + end + + before { allow(CSV).to receive(:parse).and_raise('Really bad file') } + + describe '#valid?' do + it 'should be invalid' do + expect(subject.valid?).to be false + end + + it 'reports the errors' do + subject.valid? + expect(subject.errors.full_messages).to include('Could not read csv: Really bad file') + end + end + end + + context 'A file which has missing well values' do + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_missing_values.csv', + 'sequencescape/qc_file' + ) + end + + describe '#valid?' do + it 'should be invalid' do + expect(subject.valid?).to be false + end + + let(:row4_error) do + 'Transfers input amount desired is empty or contains a value that is out of range (0.0 to 50.0), in row 4 [A1]' + end + + let(:row5_error) do + 'Transfers sample volume is empty or contains a value that is out of range (0.2 to 50.0), in row 5 [B1]' + end + + let(:row6_error) do + 'Transfers diluent volume is empty or contains a value that is out of range (0.0 to 50.0), in row 7 [D1]' + end + + let(:row7_error) do + 'Transfers pcr cycles is empty or contains a value that is out of range (1 to 20), in row 8 [E1]' + end + + let(:row11_error) { 'Transfers hyb panel is empty, in row 11 [A2]' } + + it 'reports the errors' do + subject.valid? + expect(subject.errors.full_messages).to include(row4_error) + expect(subject.errors.full_messages).to include(row5_error) + expect(subject.errors.full_messages).to include(row6_error) + expect(subject.errors.full_messages).to include(row7_error) + expect(subject.errors.full_messages).to include(row11_error) + end + end + end + + context 'An invalid file' do + let(:file) { fixture_file_upload('spec/fixtures/files/test_file.txt', 'sequencescape/qc_file') } + + describe '#valid?' do + it 'should be invalid' do + expect(subject.valid?).to be false + end + + it 'reports the errors' do + subject.valid? + expect(subject.errors.full_messages).to include( + 'Plate barcode header row barcode lbl index could not be found in: \'This is an example file\'' + ) + expect(subject.errors.full_messages).to include( + 'Plate barcode header row plate barcode could not be found in: \'This is an example file\'' + ) + expect(subject.errors.full_messages).to include('Well details header row can\'t be blank') + end + end + end + + context 'An unrecognised well' do + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file_with_invalid_wells.csv', + 'sequencescape/qc_file' + ) + end + + describe '#valid?' do + it 'should be invalid' do + expect(subject.valid?).to be false + end + + it 'reports the errors' do + subject.valid? + expect(subject.errors.full_messages).to include('Transfers well contains an invalid well name: row 11 [I1]') + end + end + end + + context 'A parent plate barcode that does not match' do + subject { described_class.new(file, csv_file_config, 'DN1S') } + + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file.csv', + 'sequencescape/qc_file' + ) + end + + describe '#valid?' do + it 'should be invalid' do + expect(subject.valid?).to be false + end + + it 'reports the errors' do + subject.valid? + expect(subject.errors.full_messages).to include( + 'Plate barcode header row plate barcode The plate barcode in the file (DN2T) does not match the ' \ + 'barcode of the plate being uploaded to (DN1S), please check you have the correct file.' + ) + end + end + end +end diff --git a/spec/models/labware_creators/pcr_cycles_binned_plate_spec.rb b/spec/models/labware_creators/pcr_cycles_binned_plate_for_duplex_seq_spec.rb similarity index 64% rename from spec/models/labware_creators/pcr_cycles_binned_plate_spec.rb rename to spec/models/labware_creators/pcr_cycles_binned_plate_for_duplex_seq_spec.rb index 622608c94..4861a9273 100644 --- a/spec/models/labware_creators/pcr_cycles_binned_plate_spec.rb +++ b/spec/models/labware_creators/pcr_cycles_binned_plate_for_duplex_seq_spec.rb @@ -4,10 +4,10 @@ require 'labware_creators/base' require_relative 'shared_examples' -RSpec.describe LabwareCreators::PcrCyclesBinnedPlate, with: :uploader do +RSpec.describe LabwareCreators::PcrCyclesBinnedPlateForDuplexSeq, with: :uploader do it_behaves_like 'it only allows creation from plates' - subject { LabwareCreators::PcrCyclesBinnedPlate.new(api, form_attributes) } + subject { LabwareCreators::PcrCyclesBinnedPlateForDuplexSeq.new(api, form_attributes) } it 'should have a custom page' do expect(described_class.page).to eq 'pcr_cycles_binned_plate' @@ -16,7 +16,7 @@ let(:parent_uuid) { 'example-plate-uuid' } let(:plate_size) { 96 } - let(:well_a1) do + let(:parent_well_a1) do create( :v2_well, position: { @@ -27,7 +27,7 @@ outer_request: nil ) end - let(:well_b1) do + let(:parent_well_b1) do create( :v2_well, position: { @@ -38,7 +38,7 @@ outer_request: nil ) end - let(:well_d1) do + let(:parent_well_d1) do create( :v2_well, position: { @@ -49,7 +49,7 @@ outer_request: nil ) end - let(:well_e1) do + let(:parent_well_e1) do create( :v2_well, position: { @@ -60,7 +60,7 @@ outer_request: nil ) end - let(:well_f1) do + let(:parent_well_f1) do create( :v2_well, position: { @@ -71,7 +71,7 @@ outer_request: nil ) end - let(:well_h1) do + let(:parent_well_h1) do create( :v2_well, position: { @@ -82,7 +82,7 @@ outer_request: nil ) end - let(:well_a2) do + let(:parent_well_a2) do create( :v2_well, position: { @@ -93,7 +93,7 @@ outer_request: nil ) end - let(:well_b2) do + let(:parent_well_b2) do create( :v2_well, position: { @@ -104,7 +104,7 @@ outer_request: nil ) end - let(:well_c2) do + let(:parent_well_c2) do create( :v2_well, position: { @@ -115,7 +115,7 @@ outer_request: nil ) end - let(:well_d2) do + let(:parent_well_d2) do create( :v2_well, position: { @@ -126,7 +126,7 @@ outer_request: nil ) end - let(:well_e2) do + let(:parent_well_e2) do create( :v2_well, position: { @@ -137,7 +137,7 @@ outer_request: nil ) end - let(:well_f2) do + let(:parent_well_f2) do create( :v2_well, position: { @@ -148,7 +148,7 @@ outer_request: nil ) end - let(:well_g2) do + let(:parent_well_g2) do create( :v2_well, position: { @@ -159,7 +159,7 @@ outer_request: nil ) end - let(:well_h2) do + let(:parent_well_h2) do create( :v2_well, position: { @@ -177,28 +177,67 @@ barcode_number: '2', size: plate_size, wells: [ - well_a1, - well_b1, - well_d1, - well_e1, - well_f1, - well_h1, - well_a2, - well_b2, - well_c2, - well_d2, - well_e2, - well_f2, - well_g2, - well_h2 + parent_well_a1, + parent_well_b1, + parent_well_d1, + parent_well_e1, + parent_well_f1, + parent_well_h1, + parent_well_a2, + parent_well_b2, + parent_well_c2, + parent_well_d2, + parent_well_e2, + parent_well_f2, + parent_well_g2, + parent_well_h2 ], outer_requests: requests end let(:parent_plate_v1) { json :plate, uuid: parent_uuid, stock_plate_barcode: 2, qc_files_actions: %w[read create] } + # Create child wells in order of the requests they originated from. + # Which is to do with how the binning algorithm lays them out based on the value of PCR cycles. + let(:child_well_A2) { create(:v2_well, location: 'A2', position: { 'name' => 'A2' }, outer_request: requests[0]) } + let(:child_well_B2) { create(:v2_well, location: 'B2', position: { 'name' => 'B2' }, outer_request: requests[1]) } + let(:child_well_A1) { create(:v2_well, location: 'A1', position: { 'name' => 'A1' }, outer_request: requests[2]) } + let(:child_well_A3) { create(:v2_well, location: 'A3', position: { 'name' => 'A3' }, outer_request: requests[3]) } + let(:child_well_B3) { create(:v2_well, location: 'B3', position: { 'name' => 'B3' }, outer_request: requests[4]) } + let(:child_well_C3) { create(:v2_well, location: 'C3', position: { 'name' => 'C3' }, outer_request: requests[5]) } + let(:child_well_D3) { create(:v2_well, location: 'D3', position: { 'name' => 'D3' }, outer_request: requests[6]) } + let(:child_well_E3) { create(:v2_well, location: 'E3', position: { 'name' => 'E3' }, outer_request: requests[7]) } + let(:child_well_F3) { create(:v2_well, location: 'F3', position: { 'name' => 'F3' }, outer_request: requests[8]) } + let(:child_well_G3) { create(:v2_well, location: 'G3', position: { 'name' => 'G3' }, outer_request: requests[9]) } + let(:child_well_C2) { create(:v2_well, location: 'C2', position: { 'name' => 'C2' }, outer_request: requests[10]) } + let(:child_well_B1) { create(:v2_well, location: 'B1', position: { 'name' => 'B1' }, outer_request: requests[11]) } + let(:child_well_D2) { create(:v2_well, location: 'D2', position: { 'name' => 'D2' }, outer_request: requests[12]) } + let(:child_well_C1) { create(:v2_well, location: 'C1', position: { 'name' => 'C1' }, outer_request: requests[13]) } + let(:child_plate) do - create :v2_plate, uuid: 'child-uuid', barcode_number: '3', size: plate_size, outer_requests: requests + # Wells listed in the order here to match the order of the list of original library requests, + # i.e. the rearranged order after binning. Wells will be laid out by location so this has no + # effect on the actual layout of the plate. + create :v2_plate, + uuid: 'child-uuid', + barcode_number: '3', + size: plate_size, + wells: [ + child_well_A2, + child_well_B2, + child_well_A1, + child_well_A3, + child_well_B3, + child_well_C3, + child_well_D3, + child_well_E3, + child_well_F3, + child_well_G3, + child_well_C2, + child_well_B1, + child_well_D2, + child_well_C1 + ] end let(:library_type_name) { 'Test Library Type' } @@ -220,7 +259,7 @@ let(:form_attributes) { { purpose_uuid: child_purpose_uuid, parent_uuid: parent_uuid } } it 'can be created' do - expect(subject).to be_a LabwareCreators::PcrCyclesBinnedPlate + expect(subject).to be_a LabwareCreators::PcrCyclesBinnedPlateForDuplexSeq end end @@ -310,86 +349,86 @@ [ { 'volume' => '5.0', - 'source_asset' => well_a1.uuid, - 'target_asset' => '3-well-A2', + 'source_asset' => parent_well_a1.uuid, + 'target_asset' => child_well_A2.uuid, 'outer_request' => requests[0].uuid }, { 'volume' => '5.0', - 'source_asset' => well_b1.uuid, - 'target_asset' => '3-well-B2', + 'source_asset' => parent_well_b1.uuid, + 'target_asset' => child_well_B2.uuid, 'outer_request' => requests[1].uuid }, { 'volume' => '5.0', - 'source_asset' => well_d1.uuid, - 'target_asset' => '3-well-A1', + 'source_asset' => parent_well_d1.uuid, + 'target_asset' => child_well_A1.uuid, 'outer_request' => requests[2].uuid }, { 'volume' => '5.0', - 'source_asset' => well_e1.uuid, - 'target_asset' => '3-well-A3', + 'source_asset' => parent_well_e1.uuid, + 'target_asset' => child_well_A3.uuid, 'outer_request' => requests[3].uuid }, { 'volume' => '4.0', - 'source_asset' => well_f1.uuid, - 'target_asset' => '3-well-B3', + 'source_asset' => parent_well_f1.uuid, + 'target_asset' => child_well_B3.uuid, 'outer_request' => requests[4].uuid }, { 'volume' => '5.0', - 'source_asset' => well_h1.uuid, - 'target_asset' => '3-well-C3', + 'source_asset' => parent_well_h1.uuid, + 'target_asset' => child_well_C3.uuid, 'outer_request' => requests[5].uuid }, { 'volume' => '3.2', - 'source_asset' => well_a2.uuid, - 'target_asset' => '3-well-D3', + 'source_asset' => parent_well_a2.uuid, + 'target_asset' => child_well_D3.uuid, 'outer_request' => requests[6].uuid }, { 'volume' => '5.0', - 'source_asset' => well_b2.uuid, - 'target_asset' => '3-well-E3', + 'source_asset' => parent_well_b2.uuid, + 'target_asset' => child_well_E3.uuid, 'outer_request' => requests[7].uuid }, { 'volume' => '5.0', - 'source_asset' => well_c2.uuid, - 'target_asset' => '3-well-F3', + 'source_asset' => parent_well_c2.uuid, + 'target_asset' => child_well_F3.uuid, 'outer_request' => requests[8].uuid }, { 'volume' => '5.0', - 'source_asset' => well_d2.uuid, - 'target_asset' => '3-well-G3', + 'source_asset' => parent_well_d2.uuid, + 'target_asset' => child_well_G3.uuid, 'outer_request' => requests[9].uuid }, { 'volume' => '5.0', - 'source_asset' => well_e2.uuid, - 'target_asset' => '3-well-C2', + 'source_asset' => parent_well_e2.uuid, + 'target_asset' => child_well_C2.uuid, 'outer_request' => requests[10].uuid }, { 'volume' => '30.0', - 'source_asset' => well_f2.uuid, - 'target_asset' => '3-well-B1', + 'source_asset' => parent_well_f2.uuid, + 'target_asset' => child_well_B1.uuid, 'outer_request' => requests[11].uuid }, { 'volume' => '5.0', - 'source_asset' => well_g2.uuid, - 'target_asset' => '3-well-D2', + 'source_asset' => parent_well_g2.uuid, + 'target_asset' => child_well_D2.uuid, 'outer_request' => requests[12].uuid }, { 'volume' => '3.621', - 'source_asset' => well_h2.uuid, - 'target_asset' => '3-well-C1', + 'source_asset' => parent_well_h2.uuid, + 'target_asset' => child_well_C1.uuid, 'outer_request' => requests[13].uuid } ] diff --git a/spec/models/labware_creators/pcr_cycles_binned_plate_for_t_nano_seq_spec.rb b/spec/models/labware_creators/pcr_cycles_binned_plate_for_t_nano_seq_spec.rb new file mode 100644 index 000000000..4b604dc5b --- /dev/null +++ b/spec/models/labware_creators/pcr_cycles_binned_plate_for_t_nano_seq_spec.rb @@ -0,0 +1,483 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'labware_creators/base' +require_relative 'shared_examples' + +RSpec.describe LabwareCreators::PcrCyclesBinnedPlateForTNanoSeq, with: :uploader do + it_behaves_like 'it only allows creation from plates' + + subject { LabwareCreators::PcrCyclesBinnedPlateForTNanoSeq.new(api, form_attributes) } + + it 'should have a custom page' do + expect(described_class.page).to eq 'pcr_cycles_binned_plate_for_t_nano_seq' + end + + let(:parent_uuid) { 'parent-plate-uuid' } + let(:plate_size) { 96 } + + let(:parent_well_a1) do + create( + :v2_well, + location: 'A1', + position: { + 'name' => 'A1' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[0]], + outer_request: nil + ) + end + let(:parent_well_b1) do + create( + :v2_well, + location: 'B1', + position: { + 'name' => 'B1' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[1]], + outer_request: nil + ) + end + let(:parent_well_d1) do + create( + :v2_well, + location: 'D1', + position: { + 'name' => 'D1' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[2]], + outer_request: nil + ) + end + let(:parent_well_e1) do + create( + :v2_well, + location: 'E1', + position: { + 'name' => 'E1' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[3]], + outer_request: nil + ) + end + let(:parent_well_f1) do + create( + :v2_well, + location: 'F1', + position: { + 'name' => 'F1' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[4]], + outer_request: nil + ) + end + let(:parent_well_h1) do + create( + :v2_well, + location: 'H1', + position: { + 'name' => 'H1' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[5]], + outer_request: nil + ) + end + let(:parent_well_a2) do + create( + :v2_well, + location: 'A2', + position: { + 'name' => 'A2' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[6]], + outer_request: nil + ) + end + let(:parent_well_b2) do + create( + :v2_well, + location: 'B2', + position: { + 'name' => 'B2' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[7]], + outer_request: nil + ) + end + let(:parent_well_c2) do + create( + :v2_well, + location: 'C2', + position: { + 'name' => 'C2' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[8]], + outer_request: nil + ) + end + let(:parent_well_d2) do + create( + :v2_well, + location: 'D2', + position: { + 'name' => 'D2' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[9]], + outer_request: nil + ) + end + let(:parent_well_e2) do + create( + :v2_well, + location: 'E2', + position: { + 'name' => 'E2' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[10]], + outer_request: nil + ) + end + let(:parent_well_f2) do + create( + :v2_well, + location: 'F2', + position: { + 'name' => 'F2' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[11]], + outer_request: nil + ) + end + let(:parent_well_g2) do + create( + :v2_well, + location: 'G2', + position: { + 'name' => 'G2' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[12]], + outer_request: nil + ) + end + let(:parent_well_h2) do + create( + :v2_well, + location: 'H2', + position: { + 'name' => 'H2' + }, + qc_results: create_list(:qc_result_concentration, 1, value: 1.0), + requests_as_source: [requests[13]], + outer_request: nil + ) + end + + let(:parent_plate) do + create :v2_plate, + uuid: parent_uuid, + barcode_number: '2', + size: plate_size, + wells: [ + parent_well_a1, + parent_well_b1, + parent_well_d1, + parent_well_e1, + parent_well_f1, + parent_well_h1, + parent_well_a2, + parent_well_b2, + parent_well_c2, + parent_well_d2, + parent_well_e2, + parent_well_f2, + parent_well_g2, + parent_well_h2 + ], + outer_requests: requests + end + + let(:parent_plate_v1) { json :plate, uuid: parent_uuid, stock_plate_barcode: 2, qc_files_actions: %w[read create] } + + # Create child wells in order of the requests they originated from. + # Which is to do with how the binning algorithm lays them out based on the value of PCR cycles. + let(:child_well_A2) { create(:v2_well, location: 'A2', position: { 'name' => 'A2' }, outer_request: requests[0]) } + let(:child_well_B2) { create(:v2_well, location: 'B2', position: { 'name' => 'B2' }, outer_request: requests[1]) } + let(:child_well_A1) { create(:v2_well, location: 'A1', position: { 'name' => 'A1' }, outer_request: requests[2]) } + let(:child_well_A3) { create(:v2_well, location: 'A3', position: { 'name' => 'A3' }, outer_request: requests[3]) } + let(:child_well_B3) { create(:v2_well, location: 'B3', position: { 'name' => 'B3' }, outer_request: requests[4]) } + let(:child_well_C3) { create(:v2_well, location: 'C3', position: { 'name' => 'C3' }, outer_request: requests[5]) } + let(:child_well_D3) { create(:v2_well, location: 'D3', position: { 'name' => 'D3' }, outer_request: requests[6]) } + let(:child_well_E3) { create(:v2_well, location: 'E3', position: { 'name' => 'E3' }, outer_request: requests[7]) } + let(:child_well_F3) { create(:v2_well, location: 'F3', position: { 'name' => 'F3' }, outer_request: requests[8]) } + let(:child_well_G3) { create(:v2_well, location: 'G3', position: { 'name' => 'G3' }, outer_request: requests[9]) } + let(:child_well_C2) { create(:v2_well, location: 'C2', position: { 'name' => 'C2' }, outer_request: requests[10]) } + let(:child_well_B1) { create(:v2_well, location: 'B1', position: { 'name' => 'B1' }, outer_request: requests[11]) } + let(:child_well_D2) { create(:v2_well, location: 'D2', position: { 'name' => 'D2' }, outer_request: requests[12]) } + let(:child_well_C1) { create(:v2_well, location: 'C1', position: { 'name' => 'C1' }, outer_request: requests[13]) } + + let(:child_plate) do + # Wells listed in the order here to match the order of the list of original library requests, + # i.e. the rearranged order after binning. Wells will be laid out by location so this has no + # effect on the actual layout of the plate. + create :v2_plate, + uuid: 'child-uuid', + barcode_number: '3', + size: plate_size, + wells: [ + child_well_A2, + child_well_B2, + child_well_A1, + child_well_A3, + child_well_B3, + child_well_C3, + child_well_D3, + child_well_E3, + child_well_F3, + child_well_G3, + child_well_C2, + child_well_B1, + child_well_D2, + child_well_C1 + ] + end + + let(:library_type_name) { 'Test Library Type' } + + let(:requests) do + Array.new(14) do |i| + create :library_request, state: 'pending', uuid: "request-#{i}", library_type: library_type_name + end + end + + let(:child_purpose_uuid) { 'child-purpose' } + let(:child_purpose_name) { 'Child Purpose' } + + let(:user_uuid) { 'user-uuid' } + + context 'on new' do + has_a_working_api + + let(:form_attributes) { { purpose_uuid: child_purpose_uuid, parent_uuid: parent_uuid } } + + it 'can be created' do + expect(subject).to be_a LabwareCreators::PcrCyclesBinnedPlateForTNanoSeq + end + end + + context '#save' do + has_a_working_api + + let(:file_content) do + content = file.read + file.rewind + content + end + + let(:form_attributes) do + { purpose_uuid: child_purpose_uuid, parent_uuid: parent_uuid, user_uuid: user_uuid, file: file } + end + + let(:stub_upload_file_creation) do + stub_request(:post, api_url_for(parent_uuid, 'qc_files')) + .with( + body: file_content, + headers: { + 'Content-Type' => 'sequencescape/qc_file', + 'Content-Disposition' => 'form-data; filename="targeted_nano_seq_customer_file.csv"' + } + ) + .to_return( + status: 201, + body: json(:qc_file, filename: 'targeted_nano_seq_dil_file.csv'), + headers: { + 'content-type' => 'application/json' + } + ) + end + + let(:stub_parent_request) { stub_api_get(parent_uuid, body: parent_plate_v1) } + + before do + stub_parent_request + + create :targeted_nano_seq_customer_csv_file_upload_purpose_config, + uuid: child_purpose_uuid, + name: child_purpose_name, + library_type_name: library_type_name + + stub_v2_plate( + parent_plate, + stub_search: false, + custom_includes: + 'wells.aliquots,wells.qc_results,wells.requests_as_source.request_type,wells.aliquots.request.request_type' + ) + + stub_v2_plate(child_plate, stub_search: false) + + stub_v2_plate(child_plate, stub_search: false, custom_includes: 'wells.aliquots') + + stub_upload_file_creation + end + + context 'with an invalid file' do + let(:file) { fixture_file_upload('spec/fixtures/files/test_file.txt', 'sequencescape/qc_file') } + + it 'is false' do + expect(subject.save).to be false + end + end + + context 'binning' do + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/targeted_nano_seq/targeted_nano_seq_dil_file.csv', + 'sequencescape/qc_file' + ) + end + + let!(:plate_creation_request) do + stub_api_post( + 'plate_creations', + payload: { + plate_creation: { + parent: parent_uuid, + child_purpose: child_purpose_uuid, + user: user_uuid + } + }, + body: json(:plate_creation) + ) + end + + let!(:api_v2_post) { stub_api_v2_post('Well') } + + let!(:api_v2_post) { stub_api_v2_save('PolyMetadatum') } + + let(:transfer_requests) do + [ + { + 'volume' => '5.0', + 'source_asset' => parent_well_a1.uuid, + 'target_asset' => child_well_A2.uuid, + 'outer_request' => requests[0].uuid + }, + { + 'volume' => '5.0', + 'source_asset' => parent_well_b1.uuid, + 'target_asset' => child_well_B2.uuid, + 'outer_request' => requests[1].uuid + }, + { + 'volume' => '5.0', + 'source_asset' => parent_well_d1.uuid, + 'target_asset' => child_well_A1.uuid, + 'outer_request' => requests[2].uuid + }, + { + 'volume' => '5.0', + 'source_asset' => parent_well_e1.uuid, + 'target_asset' => child_well_A3.uuid, + 'outer_request' => requests[3].uuid + }, + { + 'volume' => '4.0', + 'source_asset' => parent_well_f1.uuid, + 'target_asset' => child_well_B3.uuid, + 'outer_request' => requests[4].uuid + }, + { + 'volume' => '5.0', + 'source_asset' => parent_well_h1.uuid, + 'target_asset' => child_well_C3.uuid, + 'outer_request' => requests[5].uuid + }, + { + 'volume' => '3.2', + 'source_asset' => parent_well_a2.uuid, + 'target_asset' => child_well_D3.uuid, + 'outer_request' => requests[6].uuid + }, + { + 'volume' => '5.0', + 'source_asset' => parent_well_b2.uuid, + 'target_asset' => child_well_E3.uuid, + 'outer_request' => requests[7].uuid + }, + { + 'volume' => '5.0', + 'source_asset' => parent_well_c2.uuid, + 'target_asset' => child_well_F3.uuid, + 'outer_request' => requests[8].uuid + }, + { + 'volume' => '5.0', + 'source_asset' => parent_well_d2.uuid, + 'target_asset' => child_well_G3.uuid, + 'outer_request' => requests[9].uuid + }, + { + 'volume' => '5.0', + 'source_asset' => parent_well_e2.uuid, + 'target_asset' => child_well_C2.uuid, + 'outer_request' => requests[10].uuid + }, + { + 'volume' => '30.0', + 'source_asset' => parent_well_f2.uuid, + 'target_asset' => child_well_B1.uuid, + 'outer_request' => requests[11].uuid + }, + { + 'volume' => '5.0', + 'source_asset' => parent_well_g2.uuid, + 'target_asset' => child_well_D2.uuid, + 'outer_request' => requests[12].uuid + }, + { + 'volume' => '3.621', + 'source_asset' => parent_well_h2.uuid, + 'target_asset' => child_well_C1.uuid, + 'outer_request' => requests[13].uuid + } + ] + end + + let!(:transfer_creation_request) do + stub_api_post( + 'transfer_request_collections', + payload: { + transfer_request_collection: { + user: user_uuid, + transfer_requests: transfer_requests + } + }, + body: '{}' + ) + end + + it 'makes the expected method calls when creating the child plate' do + # NB. because we're mocking the API call for the save of the request metadata we cannot + # check the metadata values on the requests, only that the method was triggered. + # Our child plate has 14 wells with 14 requests, so we expect the method to create metadata + # on the requests to be called 14 times. + expect(subject).to receive(:create_request_metadata).exactly(14).times + expect(subject.save!).to eq true + expect(plate_creation_request).to have_been_made + expect(transfer_creation_request).to have_been_made + end + end + end +end diff --git a/spec/models/presenters/pcr_cycles_binned_plate_using_request_metadata_presenter_spec.rb b/spec/models/presenters/pcr_cycles_binned_plate_using_request_metadata_presenter_spec.rb new file mode 100644 index 000000000..09757b85f --- /dev/null +++ b/spec/models/presenters/pcr_cycles_binned_plate_using_request_metadata_presenter_spec.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'presenters/pcr_cycles_binned_plate_using_request_metadata_presenter' +require_relative 'shared_labware_presenter_examples' + +RSpec.describe Presenters::PcrCyclesBinnedPlateUsingRequestMetadataPresenter do + has_a_working_api + + let(:purpose_name) { 'Limber example purpose' } + let(:title) { purpose_name } + let(:state) { 'pending' } + let(:summary_tab) do + [ + %w[Barcode DN1S], + ['Number of wells', '4/96'], + ['Plate type', purpose_name], + ['Current plate state', state], + ['Input plate barcode', 'DN2T'], + ['PCR Cycles', '10'], + ['Created on', '2019-06-10'] + ] + end + let(:sidebar_partial) { 'default' } + + # Create binning for 4 wells in 3 bins: + # 1 2 3 + # A * * * + # B * + + # well A1 + let(:well_a1_metadata) { build :poly_metadatum, key: 'pcr_cycles', value: '16' } + + let(:well_a1_request) { create :library_request_with_poly_metadata, poly_metadata: [well_a1_metadata] } + let(:well_a1) do + create( + :v2_well, + position: { + 'name' => 'A1' + }, + qc_results: create_list(:qc_result_concentration, 1, value: '0.6'), + outer_request: well_a1_request + ) + end + + # well A2 + let(:well_a2_metadata) { build :poly_metadatum, key: 'pcr_cycles', value: '14' } + + let(:well_a2_request) { create :library_request_with_poly_metadata, poly_metadata: [well_a2_metadata] } + let(:well_a2) do + create( + :v2_well, + position: { + 'name' => 'A2' + }, + qc_results: create_list(:qc_result_concentration, 1, value: '10.0'), + outer_request: well_a2_request + ) + end + + # well B2 + let(:well_b2_metadata) { build :poly_metadatum, key: 'pcr_cycles', value: '14' } + + let(:well_b2_request) { create :library_request_with_poly_metadata, poly_metadata: [well_b2_metadata] } + let(:well_b2) do + create( + :v2_well, + position: { + 'name' => 'B2' + }, + qc_results: create_list(:qc_result_concentration, 1, value: '12.0'), + outer_request: well_b2_request + ) + end + + # well A3 + let(:well_a3_metadata) { build :poly_metadatum, key: 'pcr_cycles', value: '12' } + + let(:well_a3_request) { create :library_request_with_poly_metadata, poly_metadata: [well_a3_metadata] } + let(:well_a3) do + create( + :v2_well, + position: { + 'name' => 'A3' + }, + qc_results: create_list(:qc_result_concentration, 1, value: '20.0'), + outer_request: well_a3_request + ) + end + + let(:labware) do + build :v2_plate, + purpose_name: purpose_name, + state: state, + barcode_number: 1, + pool_sizes: [], + wells: [well_a1, well_a2, well_b2, well_a3], + # outer_requests: requests, + created_at: '2019-06-10 12:00:00 +0100' + end + + # let(:requests) { Array.new(4) { |i| create :library_request, state: 'started', uuid: "request-#{i}" } } + + let(:warnings) { {} } + let(:label_class) { 'Labels::PlateLabel' } + + before do + stub_v2_plate( + labware, + stub_search: false, + custom_includes: 'wells.aliquots,wells.qc_results,wells.aliquots.request.poly_metadata' + ) + end + + subject(:presenter) { Presenters::PcrCyclesBinnedPlateUsingRequestMetadataPresenter.new(api: api, labware: labware) } + + context 'when binning' do + it_behaves_like 'a labware presenter' + + context 'pcr cycles binned plate display' do + it 'should create a key for the bins that will be displayed' do + # NB. contains min/max because just using bins template, but fields not needed in presentation + expected_bins_key = [ + { 'colour' => 1, 'pcr_cycles' => 16 }, + { 'colour' => 2, 'pcr_cycles' => 14 }, + { 'colour' => 3, 'pcr_cycles' => 12 } + ] + + expect(presenter.bins_key).to eq(expected_bins_key) + end + + it 'should create bin details which will be used to colour and annotate the well aliquots' do + expected_bin_details = { + 'A1' => { + 'colour' => 1, + 'pcr_cycles' => 16 + }, + 'A2' => { + 'colour' => 2, + 'pcr_cycles' => 14 + }, + 'A3' => { + 'colour' => 3, + 'pcr_cycles' => 12 + }, + 'B2' => { + 'colour' => 2, + 'pcr_cycles' => 14 + } + } + + expect(presenter.bin_details).to eq(expected_bin_details) + end + end + end +end diff --git a/spec/models/presenters/pcr_cycles_binned_plate_presenter_spec.rb b/spec/models/presenters/pcr_cycles_binned_plate_using_well_metadata_presenter_spec.rb similarity index 92% rename from spec/models/presenters/pcr_cycles_binned_plate_presenter_spec.rb rename to spec/models/presenters/pcr_cycles_binned_plate_using_well_metadata_presenter_spec.rb index d86803cb0..b6e5554bb 100644 --- a/spec/models/presenters/pcr_cycles_binned_plate_presenter_spec.rb +++ b/spec/models/presenters/pcr_cycles_binned_plate_using_well_metadata_presenter_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true require 'rails_helper' -require 'presenters/pcr_cycles_binned_plate_presenter' +require 'presenters/pcr_cycles_binned_plate_using_well_metadata_presenter' require_relative 'shared_labware_presenter_examples' -RSpec.describe Presenters::PcrCyclesBinnedPlatePresenter do +RSpec.describe Presenters::PcrCyclesBinnedPlateUsingWellMetadataPresenter do has_a_working_api let(:purpose_name) { 'Limber example purpose' } @@ -86,7 +86,7 @@ before { stub_v2_plate(labware, stub_search: false, custom_includes: 'wells.aliquots,wells.qc_results') } - subject(:presenter) { Presenters::PcrCyclesBinnedPlatePresenter.new(api: api, labware: labware) } + subject(:presenter) { Presenters::PcrCyclesBinnedPlateUsingWellMetadataPresenter.new(api: api, labware: labware) } context 'when binning' do it_behaves_like 'a labware presenter' diff --git a/spec/views/exports/targeted_nanoseq_al_lib_concentrations_for_customer.csv.erb_spec.rb b/spec/views/exports/targeted_nanoseq_al_lib_concentrations_for_customer.csv.erb_spec.rb index facb5a6ff..5fad93dca 100644 --- a/spec/views/exports/targeted_nanoseq_al_lib_concentrations_for_customer.csv.erb_spec.rb +++ b/spec/views/exports/targeted_nanoseq_al_lib_concentrations_for_customer.csv.erb_spec.rb @@ -35,26 +35,10 @@ 'Input amount desired', 'Sample volume', 'Diluent volume', - 'PCR cycles', - 'Submit for sequencing (Y/N)?', - 'Sub-Pool', - 'Coverage' + 'Hyb Panel' ], - [ - 'A1', - '1.5', - well_a1_sanger_sample_id, - well_a1_supplier_name, - (1.5 * 25).to_s, - nil, - nil, - nil, - nil, - nil, - nil, - nil - ], - ['B1', '1.5', well_b1_sanger_sample_id, well_b1_supplier_name, (1.5 * 25).to_s, nil, nil, nil, nil, nil, nil, nil] + ['A1', '1.5', well_a1_sanger_sample_id, well_a1_supplier_name, (1.5 * 25).to_s, nil, nil, nil, nil], + ['B1', '1.5', well_b1_sanger_sample_id, well_b1_supplier_name, (1.5 * 25).to_s, nil, nil, nil, nil] ] end diff --git a/spec/views/exports/targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling.csv.erb_spec.rb b/spec/views/exports/targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling.csv.erb_spec.rb deleted file mode 100644 index 78ad3d0ac..000000000 --- a/spec/views/exports/targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling.csv.erb_spec.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'exports/targeted_nanoseq_pcr_xp_concentrations_for_custom_pooling.csv.erb' do - has_a_working_api - - let(:qc_result_options) { { value: 1.5, key: 'concentration', units: 'ng/ul' } } - - let(:well_a1) do - create(:v2_well, position: { 'name' => 'A1' }, qc_results: create_list(:qc_result, 1, qc_result_options)) - end - let(:well_b1) do - create(:v2_well, position: { 'name' => 'B1' }, qc_results: create_list(:qc_result, 1, qc_result_options)) - end - - let(:ancestor_well_a1) do - create( - :v2_well, - position: { - 'name' => 'A1' - }, - qc_results: create_list(:qc_result, 1, qc_result_options), - submit_for_sequencing: true, - sub_pool: 1, - coverage: 15 - ) - end - let(:ancestor_well_b1) do - create( - :v2_well, - position: { - 'name' => 'B1' - }, - qc_results: create_list(:qc_result, 1, qc_result_options), - submit_for_sequencing: false - ) - end - let(:labware) { create(:v2_plate, wells: [well_a1, well_b1], pool_sizes: [1, 1]) } - let(:ancestor_labware) { create(:v2_plate, wells: [ancestor_well_a1, ancestor_well_b1], pool_sizes: [1, 1]) } - - before do - assign(:plate, labware) - assign(:ancestor_plate, ancestor_labware) - end - - let(:expected_content) do - [ - ['Well', 'Concentration (ng/ul)', 'Submit for sequencing (Y/N)?', 'Sub-Pool', 'Coverage'], - %w[A1 1.5 Y 1 15], - ['B1', '1.5', 'N', nil, nil] - ] - end - - it 'renders the expected content' do - expect(CSV.parse(render)).to eq(expected_content) - end -end diff --git a/spec/views/exports/targeted_nanoseq_pcr_xp_merged_file.csv.erb_spec.rb b/spec/views/exports/targeted_nanoseq_pcr_xp_merged_file.csv.erb_spec.rb new file mode 100644 index 000000000..40fe4f7c1 --- /dev/null +++ b/spec/views/exports/targeted_nanoseq_pcr_xp_merged_file.csv.erb_spec.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'exports/targeted_nanoseq_pcr_xp_merged_file.csv.erb' do + has_a_working_api + + let(:qc_result_options_a1) { { value: 1.5, key: 'concentration', units: 'ng/ul' } } + let(:qc_result_options_b1) { { value: 1.2, key: 'concentration', units: 'ng/ul' } } + + let(:req1_pm1) { build :poly_metadatum, key: 'original_plate_barcode', value: 'BC1' } + let(:req1_pm2) { build :poly_metadatum, key: 'original_well_id', value: 'A1' } + let(:req1_pm3) { build :poly_metadatum, key: 'concentration_nm', value: 1.2 } + let(:req1_pm4) { build :poly_metadatum, key: 'input_amount_available', value: 23.1 } + let(:req1_pm5) { build :poly_metadatum, key: 'input_amount_desired', value: 34.2 } + let(:req1_pm6) { build :poly_metadatum, key: 'hyb_panel', value: 'Test hyb panel' } + + let(:request1) do + create :library_request_with_poly_metadata, + poly_metadata: [req1_pm1, req1_pm2, req1_pm3, req1_pm4, req1_pm5, req1_pm6] + end + + let(:req2_pm1) { build :poly_metadatum, key: 'original_plate_barcode', value: 'BC1' } + let(:req2_pm2) { build :poly_metadatum, key: 'original_well_id', value: 'B1' } + let(:req2_pm3) { build :poly_metadatum, key: 'concentration_nm', value: 1.0 } + let(:req2_pm4) { build :poly_metadatum, key: 'input_amount_available', value: 21.6 } + let(:req2_pm5) { build :poly_metadatum, key: 'input_amount_desired', value: 35.7 } + let(:req2_pm6) { build :poly_metadatum, key: 'hyb_panel', value: 'Test hyb panel' } + + let(:request2) do + create :library_request_with_poly_metadata, + poly_metadata: [req2_pm1, req2_pm2, req2_pm3, req2_pm4, req2_pm5, req2_pm6] + end + + let(:well_a1) do + create( + :v2_well, + location: 'A1', + position: { + 'name' => 'A1' + }, + qc_results: create_list(:qc_result, 1, qc_result_options_a1), + outer_request: request1 + ) + end + let(:well_b1) do + create( + :v2_well, + location: 'B1', + position: { + 'name' => 'B1' + }, + qc_results: create_list(:qc_result, 1, qc_result_options_b1), + outer_request: request2 + ) + end + + let(:labware) { create(:v2_plate, wells: [well_a1, well_b1], pool_sizes: [1, 1]) } + + let(:well_a1_sanger_id) { well_a1.aliquots.first.sample.sanger_sample_id } + let(:well_b1_sanger_id) { well_b1.aliquots.first.sample.sanger_sample_id } + + let(:well_a1_supplier_name) { well_a1.aliquots.first.sample.sample_metadata.supplier_name } + let(:well_b1_supplier_name) { well_b1.aliquots.first.sample.sample_metadata.supplier_name } + + before do + assign(:plate, labware) + well_a1.aliquots.first.request = request1 + well_b1.aliquots.first.request = request2 + end + + # NB. poly_metadata values are strings, so all values from poly_metadata will come out as strings in the csv + let(:expected_content) do + [ + [ + 'Original Plate Barcode', + 'Original Well ID', + 'Concentration (nM)', + 'Sanger Sample ID', + 'Supplier Sample Name', + 'Input amount available (fmol)', + 'Input amount desired (fmol)', + 'New Plate Barcode', + 'New Well ID', + 'Concentration (ng/ul)', + 'Hyb Panel' + ], + [ + 'BC1', + 'A1', + '1.2', + well_a1_sanger_id, + well_a1_supplier_name, + '23.1', + '34.2', + labware.human_barcode, + 'A1', + '1.5', + 'Test hyb panel' + ], + [ + 'BC1', + 'B1', + '1.0', + well_b1_sanger_id, + well_b1_supplier_name, + '21.6', + '35.7', + labware.human_barcode, + 'B1', + '1.2', + 'Test hyb panel' + ] + ] + end + + it 'renders the expected content' do + expect(CSV.parse(render)).to eq(expected_content) + end + + context 'when the wells are rearranged by binning, it orders correctly by original plate and well id' do + let(:well_a1) do + create( + :v2_well, + location: 'A1', + position: { + 'name' => 'A1' + }, + qc_results: create_list(:qc_result, 1, qc_result_options_a1), + outer_request: request2 + ) + end + let(:well_b1) do + create( + :v2_well, + location: 'B1', + position: { + 'name' => 'B1' + }, + qc_results: create_list(:qc_result, 1, qc_result_options_b1), + outer_request: request1 + ) + end + + let(:expected_content) do + [ + [ + 'Original Plate Barcode', + 'Original Well ID', + 'Concentration (nM)', + 'Sanger Sample ID', + 'Supplier Sample Name', + 'Input amount available (fmol)', + 'Input amount desired (fmol)', + 'New Plate Barcode', + 'New Well ID', + 'Concentration (ng/ul)', + 'Hyb Panel' + ], + [ + 'BC1', + 'A1', + '1.2', + well_b1_sanger_id, + well_b1_supplier_name, + '23.1', + '34.2', + labware.human_barcode, + 'B1', + '1.2', + 'Test hyb panel' + ], + [ + 'BC1', + 'B1', + '1.0', + well_a1_sanger_id, + well_a1_supplier_name, + '21.6', + '35.7', + labware.human_barcode, + 'A1', + '1.5', + 'Test hyb panel' + ] + ] + end + + before do + well_a1.aliquots.first.request = request2 + well_b1.aliquots.first.request = request1 + end + + it 'renders the expected content' do + expect(CSV.parse(render)).to eq(expected_content) + end + end +end