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 barcode |
+ DN12345678A |
+
+
+ |
+
+
+ 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 | 15.2 | 101 | Smp 1 | 45.3 | 50.0 | 5.0 | 20.0 | 12 | My Hyb Panel Name |
+ B1 | 0.12 | 102 | Smp 2 | 7.1 | 50.0 | 25.0 | 0.0 | 16 | My Hyb Panel Name |
+ C1 | 8.7 | 103 | Smp 3 | 30.4 | 50.0 | 10.0 | 15.0 | 12 | My Hyb Panel Name |
+ D1 | 21.3 | 104 | Smp 4 | 25.0 | 50.0 | 4.2 | 18.8 | 12 | My Hyb Panel Name |
+ E1 | 11.8 | 105 | Smp 5 | 16.9 | 50.0 | 8.7 | 16.3 | 14 | My Hyb Panel Name |
+ F1 | 9.1 | 106 | Smp 6 | 36.2 | 50.0 | 9.6 | 17.1 | 14 | My Hyb Panel Name |
+ G1 | 0.03 | 107 | Smp 7 | 19.7 | 50.0 | 25.0 | 0.0 | 12 | My Hyb Panel Name |
+ H1 | 1.8 | 108 | Smp 8 | 16.6 | 50.0 | 23.5 | 1.5 | 14 | My Hyb Panel Name |
+ A2 | 7.6 | 109 | Smp 9 | 42.2 | 50.0 | 12.6 | 12.4 | 16 | My Hyb Panel Name |
+ B2 | 14.2 | 110 | Smp 10 | 29.5 | 50.0 | 9.5 | 15.4 | 12 | My Hyb Panel Name |
+
+
+ In this example we will generate 3 bins:
+
+ - 16 cycles: Source wells B1, A2 into A1 and B1
+ - 14 cycles: Source wells E1, F1, H1 into A2 to C2
+ - 12 cycles: Source wells A1, C1, D1, G1, B2 into A3 to E3
+
+ 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