diff --git a/app/controllers/creation_controller.rb b/app/controllers/creation_controller.rb
index 6db5f6de9..ac17e3835 100644
--- a/app/controllers/creation_controller.rb
+++ b/app/controllers/creation_controller.rb
@@ -90,7 +90,7 @@ def params_purpose_uuid
end
def parent_uuid
- params[:limber_tube_id] || params[:limber_plate_id]
+ params[:limber_tube_id] || params[:limber_plate_id] || params[:limber_tube_rack_id]
end
def extract_error_messages_from_api_exception(api_message)
diff --git a/app/frontend/stylesheets/limber/screen.scss b/app/frontend/stylesheets/limber/screen.scss
index 968efaa2a..85db8eca0 100644
--- a/app/frontend/stylesheets/limber/screen.scss
+++ b/app/frontend/stylesheets/limber/screen.scss
@@ -261,6 +261,7 @@ nav.robots-list {
.robots-button,
.create-plate-button,
.create-tube-button,
+.create-tube_rack-button,
.csv-link,
.create-submission-button {
@extend .btn;
@@ -278,6 +279,7 @@ nav.robots-list {
.other-actions {
.work-completion-button,
.create-plate-button,
+ .create-tube_rack-button,
.create-tube-button {
@extend .btn-secondary;
}
@@ -285,7 +287,8 @@ nav.robots-list {
.suggested-actions {
.work-completion-button,
.create-plate-button,
- .create-tube-button {
+ .create-tube-button,
+ .create-tube_rack-button {
@extend .btn-success;
}
}
diff --git a/app/helpers/page_helper.rb b/app/helpers/page_helper.rb
index 11e5ba153..a3bb45818 100644
--- a/app/helpers/page_helper.rb
+++ b/app/helpers/page_helper.rb
@@ -53,6 +53,7 @@ def jumbotron(jumbotron_id = nil, options = {}, &)
# eg. state_badge('pending')
# Pending
def state_badge(state, title: 'Labware State')
+ return if state.blank? # added as TubeRack has a nil state
tag.span(state.titleize, class: "state-badge #{state}", title: title, data: { toggle: 'tooltip' })
end
diff --git a/app/models/concerns/presenters/creation_behaviour.rb b/app/models/concerns/presenters/creation_behaviour.rb
index 2e0ba7204..6e0771d6b 100644
--- a/app/models/concerns/presenters/creation_behaviour.rb
+++ b/app/models/concerns/presenters/creation_behaviour.rb
@@ -15,6 +15,10 @@ def compatible_tube_purposes
construct_buttons(purposes_of_type('tube'))
end
+ def compatible_tube_rack_purposes
+ construct_buttons(purposes_of_type('tube_rack'))
+ end
+
private
# Eventually this will end up on our labware_creators/creations module
diff --git a/app/models/labware_creators.rb b/app/models/labware_creators.rb
index c1e7e22d3..b9dc89b27 100644
--- a/app/models/labware_creators.rb
+++ b/app/models/labware_creators.rb
@@ -39,8 +39,16 @@ def custom_form?
# limber_plate_children (Plate -> Plate) (plate_creation#create)
# limber_plate_tubes (Plate -> Tube) (tube_creation#create)
+ # limber_plate_tube_racks (Plate -> TubeRack) (tube_rack_creation#create)
# limber_tube_children (Tube -> Plate) (nothing - want to be plate_creation#create)
# limber_tube_tubes (Tube -> Tube) (tube_creation#create)
+ # limber_tube_tube_racks (Tube -> TubeRack) (tube_rack_creation#create)
+
+ # Returns the ActiveModel::Name instance for the given type.
+ # This method maps the type to the corresponding model class and returns an ActiveModel::Name instance.
+ #
+ # @return [ActiveModel::Name] the ActiveModel::Name instance for the given type.
+ # @raise [StandardError] if the type is unknown.
def model_name
case type
# TODO: can we rename 'child' to 'plate' please? see routes.rb
@@ -48,6 +56,8 @@ def model_name
::ActiveModel::Name.new(Limber::Plate, nil, 'child')
when 'tube'
::ActiveModel::Name.new(Limber::Tube, nil, 'tube')
+ when 'tube_rack'
+ ::ActiveModel::Name.new(Limber::TubeRack, nil, 'tube_rack')
else
raise StandardError, "Unknown type #{type}"
end
diff --git a/app/models/labware_creators/donor_pooling_plate.rb b/app/models/labware_creators/donor_pooling_plate.rb
index 14e5d42d1..43a0d13b5 100644
--- a/app/models/labware_creators/donor_pooling_plate.rb
+++ b/app/models/labware_creators/donor_pooling_plate.rb
@@ -152,7 +152,7 @@ def barcodes=(barcodes)
def calculated_number_of_pools
return if source_wells_for_pooling.blank?
- # div enfoces integer division
+ # div enforces integer division
source_wells_for_pooling.count.div(number_of_samples_per_pool)
end
diff --git a/app/models/labware_creators/plate_split_to_tube_racks.rb b/app/models/labware_creators/plate_split_to_tube_racks.rb
index ca7208100..296421013 100644
--- a/app/models/labware_creators/plate_split_to_tube_racks.rb
+++ b/app/models/labware_creators/plate_split_to_tube_racks.rb
@@ -48,7 +48,7 @@ class PlateSplitToTubeRacks < Base
self.attributes += %i[sequencing_file contingency_file]
attr_accessor :sequencing_file, :contingency_file
- attr_reader :child_sequencing_tubes, :child_contingency_tubes
+ attr_reader :tube_rack_attributes, :child_tube_racks
validates_nested :well_filter
@@ -71,6 +71,9 @@ class PlateSplitToTubeRacks < Base
PARENT_PLATE_INCLUDES =
'wells.aliquots,wells.aliquots.sample,wells.downstream_tubes,wells.downstream_tubes.custom_metadatum_collection'
+ SEQ_TUBE_RACK_NAME = 'SEQ Tube Rack'
+ SPR_TUBE_RACK_NAME = 'SPR Tube Rack'
+ DEFAULT_TUBE_RACK_SIZE = '96'
def validate_file_presence
if sequencing_file.blank?
@@ -108,27 +111,49 @@ def labware_wells
#
# @return [Boolean] true if the child tubes were created successfully.
def create_labware!
- @child_sequencing_tubes = create_child_sequencing_tubes
- @child_contingency_tubes = create_child_contingency_tubes
- add_child_tube_metadata
+ @child_tube_racks = create_child_tubes_and_racks
+
+ if child_tube_racks.blank?
+ errors.add(:base, 'Failed to create child tube racks and tubes, nothing returned from API creation call')
+ return false
+ end
+
perform_transfers
true
end
- # Creates a single child sequencing tube for each parent well containing a unique sample.
- #
- # @return [Array] The child sequencing tubes.
- def create_child_sequencing_tubes
- create_tubes(sequencing_tube_purpose_uuid, parent_wells_for_sequencing.length, sequencing_tube_attributes)
- end
-
- # Creates a child contingency tube for each parent well not already assigned to a sequencing tube.
- #
- # @return [Array] The child contingency tubes.
- def create_child_contingency_tubes
- return [] if require_sequencing_tubes_only?
-
- create_tubes(contingency_tube_purpose_uuid, parent_wells_for_contingency.length, contingency_tube_attributes)
+ # @example [
+ # {
+ # :tube_rack_name=>"Seq Tube Rack",
+ # :tube_rack_barcode=>"TR00000001",
+ # :tube_rack_purpose_uuid=>"0ab4c9cc-4dad-11ef-8ca3-82c61098d1a1",
+ # :racked_tubes=>[
+ # {
+ # :tube_barcode=>"SQ45303801",
+ # :tube_name=>"SEQ:NT749R:A1",
+ # :tube_purpose_uuid=>"0ab4c9cc-4dad-11ef-8ca3-82c61098d1a1",
+ # :tube_position=>"A1",
+ # :parent_uuids=>["bd49e7f8-80a1-11ef-bab6-82c61098d1a0"]
+ # },
+ # etc... more tubes
+ # ]
+ # },
+ # etc... second rack for contingency tubes
+ def generate_tube_rack_attributes
+ @tube_rack_attributes = []
+ tube_rack_attributes << generate_sequencing_tube_rack_attributes(tube_rack_attributes)
+ return if require_sequencing_tubes_only?
+ tube_rack_attributes << generate_contingency_tube_rack_attributes(tube_rack_attributes)
+ end
+
+ def create_child_tubes_and_racks
+ # create an array of tube rack details, including the tubes
+ generate_tube_rack_attributes
+
+ Sequencescape::Api::V2::SpecificTubeRackCreation
+ .create!(parent_uuids: [parent_uuid], tube_rack_attributes: tube_rack_attributes, user_uuid: user_uuid)
+ .children # returns list of api v2 TubeRacks
+ .index_by(&:name)
end
# Creates transfer requests for the given transfer request attributes and performs the transfers.
@@ -138,12 +163,14 @@ def perform_transfers
api.transfer_request_collection.create!(user: user_uuid, transfer_requests: transfer_request_attributes)
end
- # We will create multiple child tubes, so redirect to the parent plate
+ # We will create multiple child tube racks, redirect back to the parent plate
def redirection_target
+ # NB. if we want to change this to the first tube rack use: child_tube_racks[SEQ_TUBE_RACK_NAME]
parent
end
- # Display the children tab in the plate view so we see the child tubes listed.
+ # We will want to see the list of tubes in the rack
+ # TODO: as these are racked_tubes and not child tubes, does the tube rack presenter have a relatives tab?
def anchor
'relatives_tab'
end
@@ -194,15 +221,11 @@ def check_tube_rack_barcodes_differ_between_files
)
end
+ # Checks if the sequencing tube rack barcode is the same as the contingency tube rack barcode.
+ #
+ # @return [Boolean] true if the barcodes are the same, false otherwise
def same_tube_rack_barcode?
- seq_tube_rack = extract_tube_rack_barcode(sequencing_csv_file)
- cont_tube_rack = extract_tube_rack_barcode(contingency_csv_file)
-
- seq_tube_rack == cont_tube_rack
- end
-
- def extract_tube_rack_barcode(file)
- file.position_details.values.first['tube_rack_barcode']
+ sequencing_tube_rack_barcode == contingency_tube_rack_barcode
end
# Validation to compare the tube barcodes in the two files to check for duplication
@@ -311,13 +334,16 @@ def ancestor_stock_tubes
@ancestor_stock_tubes ||= locate_ancestor_tubes
end
+ def stock_labware_purpose_name
+ @stock_labware_purpose_name ||= purpose_config.dig(:creator_class, :args, :ancestor_stock_tube_purpose_name)
+ end
+
# Locates the ancestor stock tubes for the parent wells.
#
# @return [Hash{String => Sequencescape::Api::V2::Tube}] A hash of ancestor stock tubes, keyed by sample UUID.
def locate_ancestor_tubes
- purpose_name = purpose_config[:ancestor_stock_tube_purpose_name]
+ ancestor_results = parent.ancestors.where(purpose_name: stock_labware_purpose_name)
- ancestor_results = parent.ancestors.where(purpose_name:)
return {} if ancestor_results.blank?
ancestor_results.each_with_object({}) do |ancestor_result, tube_list|
@@ -348,6 +374,32 @@ def num_contingency_tubes
@num_contingency_tubes ||= contingency_csv_file&.position_details&.length || 0
end
+ # Retrieves the tube rack barcode from the sequencing CSV file.
+ #
+ # @return [String] the tube rack barcode extracted from the sequencing CSV file
+ # @raise [KeyError, NoMethodError] if the expected keys or methods are not present in the sequencing CSV file
+ def sequencing_tube_rack_barcode
+ @sequencing_tube_rack_barcode ||= extract_tube_rack_barcode(sequencing_csv_file)
+ end
+
+ # Retrieves the tube rack barcode from the contingency CSV file.
+ #
+ # @return [String] the tube rack barcode extracted from the contingency CSV file
+ # @raise [KeyError, NoMethodError] if the expected keys or methods are not present in the contingency CSV file
+ def contingency_tube_rack_barcode
+ @contingency_tube_rack_barcode ||= extract_tube_rack_barcode(contingency_csv_file)
+ end
+
+ # Extracts the tube rack barcode from the given file.
+ # This method assumes that the file has a `position_details` attribute, which is a hash.
+ #
+ # @param file [Object] the file object containing position details
+ # @return [String] the tube rack barcode extracted from the file
+ # @raise [KeyError, NoMethodError] if the expected keys or methods are not present in the file object
+ def extract_tube_rack_barcode(file)
+ file.position_details.values.first['tube_rack_barcode']
+ end
+
# Uploads the sequencing and contingency tube rack scan CSV files to the parent plate using api v1.
#
# @return [void]
@@ -400,25 +452,6 @@ def parent_wells_for_contingency
well_filter.filtered.filter_map { |well, _ignore| well unless parent_wells_for_sequencing.include?(well) }
end
- # Creates a specified number of tubes with the given attributes and returns a hash of the created tubes indexed
- # by name.
- #
- # @param tube_purpose_uuid [String] The UUID of the tube purpose to use for the created tubes.
- # @param number_of_tubes [Integer] The number of tubes to create.
- # @param tube_attributes [Hash] A hash of attributes to use for the created tubes.
- # @return [Hash] A hash of the created tubes indexed by name.
- def create_tubes(tube_purpose_uuid, number_of_tubes, tube_attributes)
- Sequencescape::Api::V2::SpecificTubeCreation
- .create!(
- child_purpose_uuids: [tube_purpose_uuid] * number_of_tubes,
- parent_uuids: [parent_uuid],
- tube_attributes: tube_attributes,
- user_uuid: user_uuid
- )
- .children
- .index_by(&:name)
- end
-
# Returns the name of the sequencing tube purpose based on the current purpose configuration.
#
# @return [String] The name of the sequencing tube purpose.
@@ -467,6 +500,55 @@ def contingency_tube_purpose_uuid
Settings.purpose_uuids[contingency_tube_purpose_name]
end
+ # Returns the name of the sequencing tube rack purpose based on the current purpose configuration.
+ #
+ # @return [String] The name of the sequencing tube rack purpose.
+ def sequencing_tube_rack_purpose_name
+ @sequencing_tube_rack_purpose_name ||=
+ purpose_config.dig(:creator_class, :args, :child_seq_tube_rack_purpose_name)
+ end
+
+ # Returns the UUID of the sequencing tube rack purpose based on the current purpose configuration.
+ #
+ # @return [Sequencescape::Api::V2::TubeRackPurpose] The sequencing tube rack purpose.
+ def sequencing_tube_rack_purpose
+ unless sequencing_tube_rack_purpose_name
+ raise "Missing purpose configuration argument 'child_seq_tube_rack_purpose_name'"
+ end
+
+ Sequencescape::Api::V2::TubeRackPurpose.find(name: sequencing_tube_rack_purpose_name).first
+ end
+
+ def sequencing_tube_rack_purpose_uuid
+ @sequencing_tube_rack_purpose_uuid ||= sequencing_tube_rack_purpose.uuid
+ end
+
+ # Returns the name of the contingency tube rack purpose based on the current purpose configuration.
+ #
+ # @return [String] The name of the contingency tube rack purpose.
+ def contingency_tube_rack_purpose_name
+ @contingency_tube_rack_purpose_name ||=
+ purpose_config.dig(:creator_class, :args, :child_spare_tube_rack_purpose_name)
+ end
+
+ # Returns the contingency tuberack purpose based on the current purpose configuration.
+ #
+ # @return [Sequencescape::Api::V2::TubeRackPurpose] The contingency tuberack purpose.
+ def contingency_tube_rack_purpose
+ unless contingency_tube_rack_purpose_name
+ raise "Missing purpose configuration argument 'child_spare_tube_rack_purpose_name'"
+ end
+
+ Sequencescape::Api::V2::TubeRackPurpose.find(name: contingency_tube_rack_purpose_name).first
+ end
+
+ # Returns the UUID of the contingency tube rack purpose.
+ #
+ # @return [String] The UUID of the contingency tube rack purpose.
+ def contingency_tube_rack_purpose_uuid
+ @contingency_tube_rack_purpose_uuid ||= contingency_tube_rack_purpose.uuid
+ end
+
# Returns the human-readable barcode of the ancestor (supplier) source tube for the given sample UUID.
#
# @param sample_uuid [String] The UUID of the sample to find the ancestor tube for.
@@ -484,17 +566,37 @@ def ancestor_tube_barcode(sample_uuid)
# Returns a hash of attributes to use for the sequencing tubes.
#
# @return [Hash] A hash of attributes to use for the sequencing tubes.
- def sequencing_tube_attributes
- @sequencing_tube_attributes ||=
- generate_tube_attributes('sequencing', sequencing_csv_file, parent_wells_for_sequencing)
+ def generate_sequencing_tube_rack_attributes(_tube_rack_attributes)
+ {
+ tube_rack_name: SEQ_TUBE_RACK_NAME,
+ tube_rack_barcode: sequencing_tube_rack_barcode,
+ tube_rack_purpose_uuid: sequencing_tube_rack_purpose_uuid,
+ racked_tubes:
+ generate_tube_attributes(
+ 'sequencing',
+ sequencing_csv_file,
+ sequencing_tube_purpose_uuid,
+ parent_wells_for_sequencing
+ )
+ }
end
# Returns a hash of attributes to use for the contingency tubes.
#
# @return [Hash] A hash of attributes to use for the contingency tubes.
- def contingency_tube_attributes
- @contingency_tube_attributes ||=
- generate_tube_attributes('contingency', contingency_csv_file, parent_wells_for_contingency)
+ def generate_contingency_tube_rack_attributes(_tube_rack_attributes)
+ {
+ tube_rack_name: SPR_TUBE_RACK_NAME,
+ tube_rack_barcode: contingency_tube_rack_barcode,
+ tube_rack_purpose_uuid: contingency_tube_rack_purpose_uuid,
+ racked_tubes:
+ generate_tube_attributes(
+ 'contingency',
+ contingency_csv_file,
+ contingency_tube_purpose_uuid,
+ parent_wells_for_contingency
+ )
+ }
end
# Returns the name prefix for child tubes based on the tube type.
@@ -519,37 +621,16 @@ def tube_name_prefix(tube_type)
name_prefix
end
- # Adds a mapping between a well and a tube name to the appropriate hash based on the tube type.
- # @param tube_type [String] The type of tube to generate attributes for ('sequencing' or 'contingency').
- # @param well [Well] The well to add the mapping for.
- # @param tube_name [String] The name of the tube to add the mapping for.
- #
- # This method adds the mapping to either the `@sequencing_wells_to_tube_names` or to the
- # `@contingency_wells_to_tube_names` hash, depending on the tube type.
- # If the hash does not exist, this method creates it.
- # @return [void]
- def add_to_well_to_tube_hash(tube_type, well, tube_name)
- if tube_type == 'sequencing'
- @sequencing_wells_to_tube_names ||= {}
- @sequencing_wells_to_tube_names[well] = tube_name
- else
- @contingency_wells_to_tube_names ||= {}
- @contingency_wells_to_tube_names[well] = tube_name
- end
- end
-
# Generates a hash of attributes to use for the tubes based on the
# current purpose configuration and the available tube positions.
- # Passes the name for each tube.
- # Passes the foreign barcode extracted from the tube rack scan upload for each tube,
- # which on the Sequencescape side sets that barcode as the primary.
# @param tube_type [String] The type of tube to generate attributes for.
# @param csv_file [CsvFile] The CSV file containing the tube rack scan data.
+ # @param tube_purpose_uuid [String] The UUID of the tube purpose to use for the tubes.
# @param wells [Array] The parent wells to generate attributes for.
#
- # @return [Hash] A hash of attributes to use for the contingency tubes.
+ # @return [Array] A array of hashes of tube attributes.
# rubocop:disable Metrics/AbcSize
- def generate_tube_attributes(tube_type, csv_file, wells)
+ def generate_tube_attributes(tube_type, csv_file, tube_purpose_uuid, wells)
# fetch the available tube positions (i.e. locations of scanned tubes for which we
# have the barcodes) e.g. ["A1", "B1", "D1"]
available_tube_posns = csv_file.position_details.keys
@@ -559,18 +640,25 @@ def generate_tube_attributes(tube_type, csv_file, wells)
wells
.zip(available_tube_posns)
.map do |well, tube_posn|
+ # NB. assumption of 1 sample per well, but only used for name generation
sample_uuid = well.aliquots.first.sample.uuid
+ # generate a human-readable name for the tube
name_for_details = name_for_details_hash(name_prefix, ancestor_tube_barcode(sample_uuid), tube_posn)
-
tube_name = name_for(name_for_details)
- add_to_well_to_tube_hash(tube_type, well, tube_name)
- { name: tube_name, foreign_barcode: csv_file.position_details[tube_posn]['tube_barcode'] }
+ {
+ tube_barcode: csv_file.position_details[tube_posn]['tube_barcode'],
+ tube_name: tube_name,
+ tube_purpose_uuid: tube_purpose_uuid,
+ tube_position: tube_posn,
+ parent_uuids: [well.uuid]
+ }
end
end
# rubocop:enable Metrics/AbcSize
+
# Returns a hash of details to use for generating a tube name based on the given prefix,
# stock tube barcode, and destination tube position.
#
@@ -591,72 +679,130 @@ def name_for(details)
"#{details[:prefix]}:#{details[:stock_tube_bc]}:#{details[:dest_tube_posn]}"
end
- # Returns an array of transfer request hashes for the filtered wells and their corresponding child tubes.
+ # Fetches the tube barcodes for a given well UUID.
#
- # @return [Array] An array of transfer request hashes.
- def transfer_request_attributes
- well_filter.filtered.filter_map do |well, additional_parameters|
- child_tube = find_child_tube(well)
-
- next unless child_tube
-
- request_hash(well.uuid, child_tube.uuid, additional_parameters)
+ # This method iterates over the tube rack attributes and selects the racked tubes
+ # whose parent UUIDs include the specified well UUID. It then extracts the tube
+ # barcodes from the selected racked tubes.
+ #
+ # @param well_uuid [String] The UUID of the well for which to fetch tube barcodes.
+ # @return [Array] An array of tube barcodes associated with the specified well UUID.
+ #
+ def fetch_tube_barcodes_for_well(well_uuid)
+ tube_rack_attributes.flat_map do |tube_rack|
+ tube_rack[:racked_tubes]
+ .select { |racked_tube| racked_tube[:parent_uuids].include?(well_uuid) }
+ .pluck(:tube_barcode)
end
end
- # Finds the child tube corresponding to the given well.
+ # Generates a hash mapping tube barcodes to their corresponding UUIDs.
+ #
+ # This method iterates over the child tube racks and their racked tubes,
+ # extracting the barcode and UUID of each tube. It then stores these values
+ # in a hash, where the keys are the human-readable barcodes and the values
+ # are the corresponding UUIDs.
#
- # @param well [Well] The well to find the child tube for.
- # @return [Tube, nil] The child tube corresponding to the given well, or nil if no child tube was found.
- def find_child_tube(well)
- if require_sequencing_tubes_only?
- @child_sequencing_tubes[@sequencing_wells_to_tube_names[well]]
- else
- @child_sequencing_tubes[@sequencing_wells_to_tube_names[well]] ||
- @child_contingency_tubes[@contingency_wells_to_tube_names[well]]
+ # @return [Hash{String => String}] A hash mapping tube barcodes to their UUIDs.
+ #
+ # Example:
+ # {
+ # "ABC123" => "uuid-1234-5678-9012",
+ # "DEF456" => "uuid-2345-6789-0123"
+ # }
+ #
+ def generate_tube_uuids_by_barcode
+ child_tube_racks.each_with_object({}) do |(_tube_rack_name, tube_rack), tube_uuids_by_barcode|
+ tube_rack.racked_tubes.each do |racked_tube|
+ tube = racked_tube.tube
+ tube_barcode = tube.barcode.human
+ tube_uuid = tube.uuid
+
+ # store barcode to uuid mapping
+ tube_uuids_by_barcode[tube_barcode] = tube_uuid
+ end
end
end
- # Adds metadata to child tubes using details from the parsed sequencing and contingency CSV files.
- #
- # @return [void]
- def add_child_tube_metadata
- add_sequencing_tube_metadata
-
- add_contingency_tube_metadata unless require_sequencing_tubes_only?
+ # Returns a hash mapping tube barcodes to their corresponding UUIDs.
+ def tube_uuids_by_barcode
+ @tube_uuids_by_barcode ||= generate_tube_uuids_by_barcode
end
- # Adds tube rack barcode and position metadata to child sequencing tubes.
+ # Generates transfer request attributes for each well and its child tubes.
#
- # @return [void]
- def add_sequencing_tube_metadata
- child_sequencing_tubes.each do |child_tube_name, child_tube|
- tube_posn = child_tube_name.split(':').last
- add_tube_metadata(child_tube, tube_posn, sequencing_csv_file.position_details[tube_posn])
- end
+ # This method iterates over the filtered wells and generates transfer request
+ # attributes for each well and its associated child tubes. It raises an error
+ # if it is unable to identify the child tube barcodes or the newly created
+ # child tube UUIDs for any well.
+ #
+ # @return [Array] An array of hashes representing the transfer request attributes.
+ #
+ # @raise [RuntimeError] If unable to identify the child tube barcodes for a well.
+ # @raise [RuntimeError] If unable to identify the newly created child tube UUID for a well.
+ #
+ # Example:
+ # [
+ # {
+ # well_uuid: "uuid-1234-5678-9012",
+ # tube_uuid: "uuid-2345-6789-0123",
+ # additional_parameters: { ... }
+ # },
+ # ...
+ # ]
+ #
+ def transfer_request_attributes
+ well_filter
+ .filtered
+ .each_with_object([]) do |(well, additional_parameters), transfer_requests|
+ well_uuid = well.uuid
+ tube_barcodes_for_well = fetch_tube_barcodes_for_well(well_uuid)
+
+ validate_tube_barcodes_for_well!(tube_barcodes_for_well, well)
+
+ tube_barcodes_for_well.each do |tube_barcode_for_well|
+ tube_uuid = fetch_tube_uuid_for_barcode(tube_barcode_for_well, well)
+ transfer_requests << request_hash(well_uuid, tube_uuid, additional_parameters)
+ end
+ end
end
- # Adds tube rack barcode and position metadata to child contingency tubes.
+ # Validates the presence of tube barcodes for a given well.
#
- # @return [void]
- def add_contingency_tube_metadata
- child_contingency_tubes.each do |child_tube_name, child_tube|
- tube_posn = child_tube_name.split(':').last
- add_tube_metadata(child_tube, tube_posn, contingency_csv_file.position_details[tube_posn])
- end
+ # This method checks if the tube_barcodes_for_well array is blank and raises
+ # an error if it is. It ensures that there are child tube barcodes for the
+ # specified well.
+ #
+ # @param tube_barcodes_for_well [Array] An array of tube barcodes for the well.
+ # @param well [Object] The well object for which to validate tube barcodes.
+ # @raise [RuntimeError] If the tube_barcodes_for_well array is blank.
+ #
+ def validate_tube_barcodes_for_well!(tube_barcodes_for_well, well)
+ return if tube_barcodes_for_well.present?
+
+ raise "Unable to identify the child tube barcodes for parent well '#{well.position[:name]}'"
end
- # Shared method for adding tube rack barcode and position metadata to child tubes.
+ # Fetches the tube UUID for a given tube barcode.
#
- # @param child_tube [Tube] The child tube to add metadata to.
- # @param tube_posn [String] The position of the child tube in the tube rack.
- # @param tube_details [Hash] The tube details hash from the tube rack scan file.
- # @return [void]
- def add_tube_metadata(child_tube, tube_posn, tube_details)
- LabwareMetadata.new(user_uuid: user_uuid, barcode: child_tube.barcode.machine).update!(
- tube_rack_barcode: tube_details['tube_rack_barcode'],
- tube_rack_position: tube_posn
- )
+ # This method retrieves the tube UUID for the specified tube barcode from the
+ # tube_uuids_by_barcode hash. It raises an error if the tube UUID is blank.
+ #
+ # @param tube_barcode_for_well [String] The barcode of the tube for which to fetch the UUID.
+ # @param well [Object] The well object associated with the tube barcode.
+ # @return [String] The UUID of the tube.
+ # @raise [RuntimeError] If the tube UUID is blank.
+ #
+ # Example:
+ # tube_uuid = fetch_tube_uuid_for_barcode("ABC123", well)
+ # # => "uuid-2345-6789-0123"
+ #
+ def fetch_tube_uuid_for_barcode(tube_barcode_for_well, well)
+ tube_uuid = tube_uuids_by_barcode[tube_barcode_for_well]
+ if tube_uuid.blank?
+ raise "Unable to identify the newly created child tube for parent well '#{well.position[:name]}'"
+ end
+ tube_uuid
end
# Generates a transfer request hash for the given source well UUID, target tube UUID, and additional parameters.
diff --git a/app/models/limber/multiplexed_library_tube.rb b/app/models/limber/multiplexed_library_tube.rb
index 7153ba2ee..5ccaee61e 100644
--- a/app/models/limber/multiplexed_library_tube.rb
+++ b/app/models/limber/multiplexed_library_tube.rb
@@ -15,6 +15,10 @@ def plate?
false
end
+ def tube_rack?
+ false
+ end
+
#
# Override the model used in form/URL helpers
# to allow us to treat tubes and multiplexed tubes
diff --git a/app/models/limber/plate.rb b/app/models/limber/plate.rb
index d0ee23af2..647253a24 100644
--- a/app/models/limber/plate.rb
+++ b/app/models/limber/plate.rb
@@ -63,4 +63,8 @@ def plate?
def tube?
false
end
+
+ def tube_rack?
+ false
+ end
end
diff --git a/app/models/limber/tube.rb b/app/models/limber/tube.rb
index 00d7b3600..fe48dc73b 100644
--- a/app/models/limber/tube.rb
+++ b/app/models/limber/tube.rb
@@ -15,4 +15,8 @@ def plate?
def tube?
true
end
+
+ def tube_rack?
+ false
+ end
end
diff --git a/app/models/limber/tube_rack.rb b/app/models/limber/tube_rack.rb
new file mode 100644
index 000000000..0e5950f44
--- /dev/null
+++ b/app/models/limber/tube_rack.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# Represents a tube rack in the system
+class Limber::TubeRack
+ def purpose
+ tube_rack_purpose
+ end
+
+ def plate?
+ false
+ end
+
+ def tube?
+ false
+ end
+
+ def tube_rack?
+ true
+ end
+end
diff --git a/app/models/state_changers.rb b/app/models/state_changers.rb
index b0e33c72e..f6fb68192 100644
--- a/app/models/state_changers.rb
+++ b/app/models/state_changers.rb
@@ -23,6 +23,17 @@ def initialize(api, labware_uuid, user_uuid)
end
# rubocop:todo Style/OptionalBooleanParameter
+
+ # This method performs a state change on the labware by creating a new state change record
+ # using the Sequencescape API. It includes details such as the contents to be changed,
+ # whether the customer accepts responsibility, the reason for the change, the target state,
+ # the target UUID, and the user UUID.
+ #
+ # @param state [String] the target state to move the labware to
+ # @param reason [String, nil] the reason for the state change (optional)
+ # @param customer_accepts_responsibility [Boolean] whether the customer accepts responsibility
+ # for the state change (default: false)
+ # @return [Sequencescape::Api::V2::StateChange] the created state change record
def move_to!(state, reason = nil, customer_accepts_responsibility = false)
Sequencescape::Api::V2::StateChange.create!(
contents: contents_for(state),
@@ -36,6 +47,14 @@ def move_to!(state, reason = nil, customer_accepts_responsibility = false)
# rubocop:enable Style/OptionalBooleanParameter
+ # This method determines the well locations that require a state change based on the target state.
+ # If the target state is not in the FILTER_FAILS_ON list, it returns nil.
+ # It filters out wells that are in the 'failed' state and collects their locations.
+ # If all wells are in the 'failed' state, it returns nil.
+ # Otherwise, it returns the locations of the wells that are not in the 'failed' state.
+ #
+ # @param target_state [String] the state to check against the FILTER_FAILS_ON list
+ # @return [Array, nil] an array of well locations requiring the state change, or nil if no change is needed
def contents_for(target_state)
return nil unless FILTER_FAILS_ON.include?(target_state)
@@ -58,6 +77,39 @@ def self.lookup_for(purpose_uuid)
details[:state_changer_class].constantize
end
+ # The tube rack state changer is used by TubeRacks.
+ # It contains racked tubes.
+ class TubeRackStateChanger < DefaultStateChanger
+ # This method determines the coordinates of tubes that require a state change based on the target state.
+ # If the target state is not in the FILTER_FAILS_ON list, it returns nil.
+ # It filters out tubes that are in the 'failed' state and collects their coordinates.
+ # If all tubes are in the 'failed' state, it returns nil.
+ # Otherwise, it returns the coordinates of the tubes that are not in the 'failed' state.
+ #
+ # @param target_state [String] the state to check against the FILTER_FAILS_ON list
+ # @return [Array, nil] an array of tube coordinates requiring the state change, or nil if no
+ # change is needed
+ def contents_for(target_state)
+ return nil unless FILTER_FAILS_ON.include?(target_state)
+
+ # determine list of tubes requiring the state change
+ # TODO: why does this check specifically for 'failed' when the FILTER_FAILS_ON is a list with several states?
+ racked_tubes_locations_filtered = labware.racked_tubes.reject { |rt| rt.tube.state == 'failed' }.map(&:coordinate)
+
+ # if no tubes are in the target state then no need to send the contents subset (state changer assumes all
+ # will change)
+ return nil if racked_tubes_locations_filtered.length == labware.racked_tubes.count
+
+ # NB. if all tubes are already in the target state then this method will return an empty array
+ # TODO: is this correct behaviour?
+ racked_tubes_locations_filtered
+ end
+
+ def labware
+ @labware ||= Sequencescape::Api::V2::TubeRack.find({ uuid: labware_uuid }).first
+ end
+ end
+
# The tube state changer is used by Tubes. It works the same way as the default
# state changer but does not need to handle a subset of wells like the plate.
class TubeStateChanger < DefaultStateChanger
@@ -129,4 +181,15 @@ def labware
@labware ||= v2_labware
end
end
+
+ # This version of the AutomaticLabwareStateChanger is used by TubeRacks.
+ class AutomaticTubeRackStateChanger < AutomaticLabwareStateChanger
+ def v2_labware
+ @v2_labware ||= Sequencescape::Api::V2.tube_rack_for_completion(labware_uuid)
+ end
+
+ def labware
+ @labware ||= v2_labware
+ end
+ end
end
diff --git a/app/sequencescape/sequencescape/api/v2.rb b/app/sequencescape/sequencescape/api/v2.rb
index 5b06efc3e..968a079d9 100644
--- a/app/sequencescape/sequencescape/api/v2.rb
+++ b/app/sequencescape/sequencescape/api/v2.rb
@@ -22,6 +22,13 @@ module Sequencescape::Api::V2
}
].freeze
+ # NB. a receptacle can have many aliquots, and aliquot.request is an array (for some reason)
+ # Sequencescape::Api::V2::TubeRack.last.racked_tubes.first.tube.receptacle.aliquots.first.request.first.request_type
+ TUBE_RACK_PRESENTER_INCLUDES = [
+ :purpose,
+ { racked_tubes: [{ tube: [:purpose, { receptacle: [{ aliquots: [{ request: [:request_type] }] }] }] }] }
+ ].freeze
+
#
# Returns a {Sequencescape::V2::Api::Labware} object with *just* the UUID, suitable for redirection
#
@@ -54,13 +61,7 @@ def self.plate_with_wells(uuid)
end
def self.tube_rack_for_presenter(query)
- TubeRack
- .includes(
- 'racked_tubes.tube.purpose,' \
- 'racked_tubes.tube.receptacle.aliquots.request.request_type'
- )
- .find(query)
- .first
+ TubeRack.includes(*TUBE_RACK_PRESENTER_INCLUDES).find(query).first
end
def self.plate_for_completion(uuid)
@@ -71,6 +72,13 @@ def self.tube_for_completion(uuid)
Tube.includes('receptacle.aliquots.request.submission,receptacle.aliquots.request.request_type').find(uuid:).first
end
+ def self.tube_rack_for_completion(uuid)
+ TubeRack
+ .includes('racked_tubes.tube.receptacle.aliquots.request.submission,receptacle.aliquots.request.request_type')
+ .find(uuid:)
+ .first
+ end
+
def self.plate_with_custom_includes(include_params, search_params)
Plate.includes(include_params).find(search_params).first
end
@@ -79,6 +87,10 @@ def self.tube_with_custom_includes(include_params, select_params, search_params)
Tube.includes(include_params).select(select_params).find(search_params).first
end
+ def self.tube_rack_with_custom_includes(include_params, search_params)
+ TubeRack.includes(include_params).find(search_params).first
+ end
+
# Retrieves results of query builder (JsonApiClient::Query::Builder) page by page
# and combines them into one list
def self.merge_page_results(query_builder)
diff --git a/app/sequencescape/sequencescape/api/v2/aliquot.rb b/app/sequencescape/sequencescape/api/v2/aliquot.rb
index 0f3c4960c..4cc3f87c8 100644
--- a/app/sequencescape/sequencescape/api/v2/aliquot.rb
+++ b/app/sequencescape/sequencescape/api/v2/aliquot.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Sequencescape::Api::V2::Aliquot < Sequencescape::Api::V2::Base # rubocop:todo Style/Documentation
- # requires shallow path otherwise get a resource not found issue
+ # requires shallow path otherwise get a resource not found issue TODO: where/when do we get this?
belongs_to :request, shallow_path: true
has_one :sample
has_one :study
diff --git a/app/sequencescape/sequencescape/api/v2/asset.rb b/app/sequencescape/sequencescape/api/v2/asset.rb
index 026458797..7195a12ba 100644
--- a/app/sequencescape/sequencescape/api/v2/asset.rb
+++ b/app/sequencescape/sequencescape/api/v2/asset.rb
@@ -21,6 +21,10 @@ def tube?
type == 'tubes'
end
+ def tube_rack?
+ type == 'tube_racks'
+ end
+
def barcode
labware_barcode
end
diff --git a/app/sequencescape/sequencescape/api/v2/base.rb b/app/sequencescape/sequencescape/api/v2/base.rb
index 933fb5686..d134a9890 100644
--- a/app/sequencescape/sequencescape/api/v2/base.rb
+++ b/app/sequencescape/sequencescape/api/v2/base.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Sequencescape::Api::V2::Base < JsonApiClient::Resource # rubocop:todo Style/Documentation
- class_attribute :plate, :tube
+ class_attribute :plate, :tube, :tube_rack
# Adjusts the parameters used for pagination. We create a custom
# class to avoid mutating the global JsonApiClient::Paginating::Paginator object
@@ -20,5 +20,6 @@ class SequencescapePaginator < JsonApiClient::Paginating::NestedParamPaginator
.authorisation
self.plate = false
self.tube = false
+ self.tube_rack = false
self.paginator = SequencescapePaginator
end
diff --git a/app/sequencescape/sequencescape/api/v2/labware.rb b/app/sequencescape/sequencescape/api/v2/labware.rb
index e4cc695e4..3f5fc29a7 100644
--- a/app/sequencescape/sequencescape/api/v2/labware.rb
+++ b/app/sequencescape/sequencescape/api/v2/labware.rb
@@ -17,9 +17,6 @@ def self.table_name
has_many :state_changes
has_many :ancestors, class_name: 'Sequencescape::Api::V2::Asset' # Having issues with polymorphism, temporary class
- # Other relationships
- # has_one :purpose via Sequencescape::Api::V2::Shared::HasPurpose
-
def self.find_all(options, includes: DEFAULT_INCLUDES)
Sequencescape::Api::V2::Labware.includes(*includes).where(options).all
end
@@ -27,7 +24,7 @@ def self.find_all(options, includes: DEFAULT_INCLUDES)
#
# Plates and tubes are handled by different URLs. This allows us to redirect
# to the expected endpoint.
- # @return [ActiveModel::Name] The resource behaves like a Limber::Tube/Limber::Plate
+ # @return [ActiveModel::Name] The resource behaves like a Limber::Tube/Limber::Plate/Limber::TubeRack
#
def model_name
case type
@@ -36,7 +33,7 @@ def model_name
when 'plates'
::ActiveModel::Name.new(Limber::Plate, false)
when 'tube_racks'
- ::ActiveModel::Name.new(Sequencescape::Api::V2::TubeRack, false, 'Limber::TubeRack')
+ ::ActiveModel::Name.new(Limber::TubeRack, false)
else
raise "Can't view #{type} in limber"
end
@@ -55,6 +52,10 @@ def tube?
type == 'tubes'
end
+ def tube_rack?
+ type == 'tube_racks'
+ end
+
# ===== stock plate / input plate barcode ======
def input_barcode
diff --git a/app/sequencescape/sequencescape/api/v2/plate.rb b/app/sequencescape/sequencescape/api/v2/plate.rb
index 8af5dfe63..3dfd2b397 100644
--- a/app/sequencescape/sequencescape/api/v2/plate.rb
+++ b/app/sequencescape/sequencescape/api/v2/plate.rb
@@ -22,9 +22,6 @@ class Sequencescape::Api::V2::Plate < Sequencescape::Api::V2::Base
has_one :custom_metadatum_collection
has_many :submission_pools
- # Other relationships
- # has_one :purpose via Sequencescape::Api::V2::Shared::HasPurpose
-
property :created_at, type: :time
property :updated_at, type: :time
diff --git a/app/sequencescape/sequencescape/api/v2/request.rb b/app/sequencescape/sequencescape/api/v2/request.rb
index ef3406a4b..c6df81c32 100644
--- a/app/sequencescape/sequencescape/api/v2/request.rb
+++ b/app/sequencescape/sequencescape/api/v2/request.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
-class Sequencescape::Api::V2::Request < Sequencescape::Api::V2::Base # rubocop:todo Style/Documentation
+# Represents a request in Limber via the Sequencescape API
+class Sequencescape::Api::V2::Request < Sequencescape::Api::V2::Base
include Sequencescape::Api::V2::Shared::HasPolyMetadata
FragmentSize = Struct.new(:from, :to)
+ has_one :request_type
has_one :submission
has_one :order
has_one :request_metadata, class_name: 'Sequencescape::Api::V2::RequestMetadata'
diff --git a/app/sequencescape/sequencescape/api/v2/request_type.rb b/app/sequencescape/sequencescape/api/v2/request_type.rb
index edd28e22d..d3293f9e1 100644
--- a/app/sequencescape/sequencescape/api/v2/request_type.rb
+++ b/app/sequencescape/sequencescape/api/v2/request_type.rb
@@ -1,4 +1,6 @@
# frozen_string_literal: true
+# Represents a request type in Limber via the Sequencescape API
class Sequencescape::Api::V2::RequestType < Sequencescape::Api::V2::Base
+ has_many :requests
end
diff --git a/app/sequencescape/sequencescape/api/v2/specific_tube_rack_creation.rb b/app/sequencescape/sequencescape/api/v2/specific_tube_rack_creation.rb
new file mode 100644
index 000000000..9f93670fd
--- /dev/null
+++ b/app/sequencescape/sequencescape/api/v2/specific_tube_rack_creation.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# Represents a specific tube rack creation in Limber via the Sequencescape API
+class Sequencescape::Api::V2::SpecificTubeRackCreation < Sequencescape::Api::V2::Base
+ has_many :children, class_name: 'Sequencescape::Api::V2::TubeRack'
+ has_one :parent, class_name: 'Sequencescape::Api::V2::Plate'
+ has_one :user, class_name: 'Sequencescape::Api::V2::User'
+end
diff --git a/app/sequencescape/sequencescape/api/v2/tube_rack.rb b/app/sequencescape/sequencescape/api/v2/tube_rack.rb
index fd39e5582..1e28cc6ae 100644
--- a/app/sequencescape/sequencescape/api/v2/tube_rack.rb
+++ b/app/sequencescape/sequencescape/api/v2/tube_rack.rb
@@ -1,25 +1,76 @@
# frozen_string_literal: true
-# Tube racks can be barcoded, and contain tubes at defined locations.
+require_dependency 'well_helpers'
+
+# Tube racks can be barcoded, and contain racked tubes at defined locations.
class Sequencescape::Api::V2::TubeRack < Sequencescape::Api::V2::Base
- include WellHelpers::Extensions
+ include WellHelpers::Extensions # obviously tube racks do not have wells, refactor the helper?
+ include Sequencescape::Api::V2::Shared::HasRequests
include Sequencescape::Api::V2::Shared::HasPurpose
include Sequencescape::Api::V2::Shared::HasBarcode
+ include Sequencescape::Api::V2::Shared::HasPolyMetadata
- has_many :racked_tubes
+ self.tube_rack = true
- property :created_at, type: :time
- property :updated_at, type: :time
+ # This is needed in order for the URL helpers to work correctly
+ def to_param
+ uuid
+ end
+
+ #
+ # Override the model used in form/URL helpers
+ # to allow us to treat old and new api the same
+ #
+ # @return [ActiveModel::Name] The resource behaves like a Limber::TubeRack
+ #
+ def model_name
+ ::ActiveModel::Name.new(Limber::TubeRack, false)
+ end
+
+ has_many :racked_tubes, class_name: 'Sequencescape::Api::V2::RackedTube'
+ has_many :parents, class_name: 'Sequencescape::Api::V2::Asset'
property :name
property :size
property :number_or_rows
property :number_of_columns
- # Other relationships
- # has_one :purpose via Sequencescape::Api::V2::Shared::HasPurpose
+ property :created_at, type: :time
+ property :updated_at, type: :time
- def model_name
- ::ActiveModel::Name.new(Sequencescape::Api::V2::TubeRack, false, 'Limber::TubeRack')
+ def stock_plate
+ nil
+ end
+
+ private
+
+ # This method iterates over all racked tubes in the tube rack and retrieves the
+ # aliquots for each associated tube. It flattens the resulting arrays into a single
+ # array and removes any nil values.
+ # Used to determine the active requests for the tube rack. See HasRequests for more details.
+ #
+ # @return [Array] An array of aliquots for the tubes in the rack.
+ #
+ # Example:
+ # aliquots = tube_rack.aliquots
+ # # => [, , ...]
+ #
+ def aliquots
+ racked_tubes.flat_map { |racked_tube| racked_tube.tube.aliquots }&.compact
+ end
+
+ # This method iterates over all racked tubes in the tube rack and retrieves the
+ # requests_as_source for each associated tube. It flattens the resulting
+ # arrays into a single array and removes any nil values.
+ # Used to determine the active requests for the tube rack. See HasRequests for more details.
+ #
+ # @return [Array] An array of requests_as_source for the tubes in the rack.
+ #
+ # Example:
+ # requests = tube_rack.requests_as_source_for_tubes
+ # # => [, , ...]
+ #
+ def requests_as_source
+ racked_tubes.flat_map { |racked_tube| racked_tube.tube.requests_as_source }&.compact
end
end
diff --git a/app/views/plates/sidebars/_default.html.erb b/app/views/plates/sidebars/_default.html.erb
index 01d15bfd0..7bac6c891 100644
--- a/app/views/plates/sidebars/_default.html.erb
+++ b/app/views/plates/sidebars/_default.html.erb
@@ -47,10 +47,11 @@
<%= simple_state_change_form(presenter) %>
<% end %>
- <% presenter.control_additional_creation do %>
- <%= render 'creation_dropdown', resource_type: 'plates', resources: presenter.compatible_plate_purposes %>
- <%= render 'creation_dropdown', resource_type: 'tubes', resources: presenter.compatible_tube_purposes %>
- <% end %>
+ <% presenter.control_additional_creation do %>
+ <%= render 'creation_dropdown', resource_type: 'plates', resources: presenter.compatible_plate_purposes %>
+ <%= render 'creation_dropdown', resource_type: 'tubes', resources: presenter.compatible_tube_purposes %>
+ <%= render 'creation_dropdown', resource_type: 'tube_racks', resources: presenter.compatible_tube_rack_purposes %>
+ <% end %>
<% presenter.control_library_passing do %>
<%= render 'work_completion_form', presenter: presenter %>
diff --git a/app/views/tube_creation/plate_split_to_tube_racks.html.erb b/app/views/tube_rack_creation/plate_split_to_tube_racks.html.erb
similarity index 100%
rename from app/views/tube_creation/plate_split_to_tube_racks.html.erb
rename to app/views/tube_rack_creation/plate_split_to_tube_racks.html.erb
diff --git a/config/initializers/json_api_casters.rb b/config/initializers/json_api_casters.rb
index d456fff4c..2301321b3 100644
--- a/config/initializers/json_api_casters.rb
+++ b/config/initializers/json_api_casters.rb
@@ -5,6 +5,10 @@
# Takes labware barcodes from the API and wraps them
class LabwareBarcodeCaster
def self.cast(value, _default)
+ return nil if value.nil?
+ return value if value.is_a?(LabwareBarcode)
+ return value if value.is_a?(String) && value.blank?
+
LabwareBarcode.new(
human: value['human_barcode'],
machine: (value['machine_barcode'] || value['ean13_barcode']).to_s,
diff --git a/config/pipelines/high_throughput_scrna_core_cell_extraction.yml b/config/pipelines/high_throughput_scrna_core_cell_extraction.yml
index c9c7aacdf..9d7260fab 100644
--- a/config/pipelines/high_throughput_scrna_core_cell_extraction.yml
+++ b/config/pipelines/high_throughput_scrna_core_cell_extraction.yml
@@ -12,10 +12,10 @@ scRNA Core Cell Extraction Seq:
request_type_key: limber_scrna_core_cell_extraction
relationships:
LRC Blood Bank: LRC PBMC Bank
- LRC PBMC Bank: LRC Bank Seq
+ LRC PBMC Bank: TR LRC Bank Seq
# This branch allows creation of extra tubes (back ups)
scRNA Core Cell Extraction Spare:
pipeline_group: scRNA Core Cell Extraction
filters: *scrna_core_cell_extraction_filters
relationships:
- LRC PBMC Bank: LRC Bank Spare
+ LRC PBMC Bank: TR LRC Bank Spare
diff --git a/config/purposes/scrna_core_cell_extraction.yml b/config/purposes/scrna_core_cell_extraction.yml
index 8039dd289..0bcd17d09 100644
--- a/config/purposes/scrna_core_cell_extraction.yml
+++ b/config/purposes/scrna_core_cell_extraction.yml
@@ -86,34 +86,51 @@ LRC PBMC Bank:
units: 'cells/ml'
default_threshold: 650000
decimal_places: 0
-# FluidX tube for freezing and output of cell banking protocol
-LRC Bank Seq:
- :asset_type: tube
- :target: SampleTube
- :type: Tube::Purpose
+# Tube rack for storing LRC Bank Seq tubes
+TR LRC Bank Seq:
+ :asset_type: tube_rack
+ :target: TubeRack
+ :size: 96
+ :type: TubeRack::Purpose
:creator_class:
name: LabwareCreators::PlateSplitToTubeRacks
args: &fluidx_tube_creation_config
child_seq_tube_purpose_name: LRC Bank Seq
child_seq_tube_name_prefix: SEQ
+ child_seq_tube_rack_purpose_name: TR LRC Bank Seq
child_spare_tube_purpose_name: LRC Bank Spare
child_spare_tube_name_prefix: SPR
- :ancestor_stock_tube_purpose_name: LRC Blood Vac
- :presenter_class: Presenters::SimpleTubePresenter
- :state_changer_class: StateChangers::AutomaticTubeStateChanger
+ child_spare_tube_rack_purpose_name: TR LRC Bank Spare
+ ancestor_stock_tube_purpose_name: LRC Blood Vac
+ :presenter_class: Presenters::TubeRackPresenter
+ :state_changer_class: StateChangers::AutomaticTubeRackStateChanger
:work_completion_request_type: 'limber_scrna_core_cell_extraction'
+# Tube rack for storing LRC Bank Spare tubes
+TR LRC Bank Spare:
+ :asset_type: tube_rack
+ :target: TubeRack
+ :size: 96
+ :type: TubeRack::Purpose
+ :creator_class:
+ name: LabwareCreators::PlateSplitToTubeRacks
+ args: *fluidx_tube_creation_config
+ :presenter_class: Presenters::TubeRackPresenter
+ :state_changer_class: StateChangers::AutomaticTubeRackStateChanger
+ :work_completion_request_type: 'limber_scrna_core_cell_extraction'
+# FluidX tube for freezing and output of cell banking protocol
+LRC Bank Seq:
+ :asset_type: tube
+ :target: SampleTube
+ :type: Tube::Purpose
+ :creator_class: LabwareCreators::Uncreatable
+ :presenter_class: Presenters::SimpleTubePresenter
# FluidX tube for freezing and output of cell banking protocol
LRC Bank Spare:
:asset_type: tube
:target: SampleTube
:type: Tube::Purpose
- :creator_class:
- name: LabwareCreators::PlateSplitToTubeRacks
- args: *fluidx_tube_creation_config
- :ancestor_stock_tube_purpose_name: LRC Blood Vac
+ :creator_class: LabwareCreators::Uncreatable
:presenter_class: Presenters::SimpleTubePresenter
- :state_changer_class: StateChangers::AutomaticTubeStateChanger
- :work_completion_request_type: 'limber_scrna_core_cell_extraction'
# FluidX tube frozen input from faculty tube, created by manifest
LRC Bank Input:
:asset_type: tube
diff --git a/config/routes.rb b/config/routes.rb
index 68bbe622e..d64c35514 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -9,6 +9,8 @@
get '/', action: :new, as: :search
post '/', action: :create, as: :perform_search
get '/ongoing_plates', action: :ongoing_plates
+
+ # TODO: do we need to add ongoing_tube_racks here?
get '/ongoing_tubes', action: :ongoing_tubes
post '/qcables', action: :qcables, as: :qcables_search
end
@@ -33,6 +35,7 @@
resources :limber_plates, controller: :plates do
resources :children, controller: :plate_creation
resources :tubes, controller: :tube_creation
+ resources :tube_racks, controller: :tube_rack_creation
resources :qc_files
resources :exports, only: :show
resources :work_completions, only: :create, module: :plates
@@ -45,6 +48,7 @@
resources :limber_tubes, controller: :tubes do
resources :children, controller: :plate_creation
resources :tubes, controller: :tube_creation
+ resources :tube_racks, controller: :tube_rack_creation
resources :qc_files, controller: :qc_files
resources :tubes_exports, only: :show, module: :tubes
resources :work_completions, only: :create, module: :tubes
@@ -53,8 +57,11 @@
resources :validate_paired_tubes, only: :index, module: :tubes
resources :limber_tube_racks, controller: :tube_racks do
+ resources :children, controller: :plate_creation
resources :qc_files, controller: :qc_files
- resources :exports, only: :show
+ # TODO: do we need to add exports and work completion code for tube racks?
+ # resources :tube_rack_exports, only: :show
+ # resources :tube_rack_work_completions, only: :create, module: :tube_racks
end
# limber_multiplexed_library_tube routes have been removed, and instead
diff --git a/lib/purpose_config.rb b/lib/purpose_config.rb
index 454fcb122..43216a5a9 100644
--- a/lib/purpose_config.rb
+++ b/lib/purpose_config.rb
@@ -59,22 +59,72 @@ def uuid
store.fetch(name).uuid
end
- # A helper class to register new TubeRack::Purpose
+ # The TubeRack class is a configuration class for defining the purpose and behavior of tube racks within the system.
+ # It inherits from PurposeConfig and sets default options and state changers specific to tube racks.
+ #
+ # Attributes:
+ # - default_state_changer: Specifies the state changer class to be used for tube racks.
+ # - default_options: A hash containing default configuration options for tube racks, including:
+ # - presenter_class: The class responsible for presenting tube racks.
+ # - creator_class: The class responsible for creating tube racks from other labwares.
+ # - default_printer_type: The default printer type for tube racks.
+ # - label_class: The class responsible for label printing for tube racks.
+ # - file_links: An array for file links associated with tube racks.
+ #
+ # Methods:
+ # - register!: Registers the tube rack purpose by creating a new TubeRackPurpose record via the Sequencescape API.
+ # It uses the name and options provided to create the record.
+ #
+ # Example:
+ # tube_rack = TubeRack.new(name: 'TR Stock 96', options: { target: 'TubeRack', type: 'TubeRack::Purpose' })
+ # tube_rack.register!
+ #
class TubeRack < PurposeConfig
- self.default_options = { default_printer_type: :tube_rack, presenter_class: 'Presenters::TubeRackPresenter' }.freeze
+ self.default_state_changer = 'StateChangers::TubeRackStateChanger'
+
+ self.default_options = {
+ presenter_class: 'Presenters::TubeRackPresenter',
+ # NB. this will need to be a more generic creator in future
+ creator_class: 'LabwareCreators::PlateSplitToTubeRacks',
+ # NB. Tube racks have etched barcodes, so don't typically need labels printed.
+ default_printer_type: :plate_a,
+ label_class: 'Labels::PlateLabel',
+ file_links: []
+ }.freeze
def register!
puts "Creating #{name}"
options_for_creation = {
name: name,
size: config.fetch(:size, 96),
- target_type: config.fetch(:target, 'TubeRack')
+ target_type: config.fetch(:target, 'TubeRack'),
+ purpose_type: config.fetch(:type, 'TubeRack::Purpose')
}
Sequencescape::Api::V2::TubeRackPurpose.create!(options_for_creation)
end
end
- class Tube < PurposeConfig # rubocop:todo Style/Documentation
+ # The Tube class is a configuration class for defining the purpose and behavior of tubes within the system.
+ # It inherits from PurposeConfig and sets default options and state changers specific to tubes.
+ #
+ # Attributes:
+ # - default_state_changer: Specifies the state changer class to be used for tubes.
+ # - default_options: A hash containing default configuration options for tubes, including:
+ # - default_printer_type: The default printer type for tubes.
+ # - presenter_class: The class responsible for presenting tubes.
+ # - creator_class: The class responsible for creating tubes from other tubes.
+ # - label_class: The class responsible for labeling tubes.
+ # - file_links: An array for file links associated with tubes.
+ #
+ # Methods:
+ # - register!: Registers the tube purpose by creating a new TubePurpose record in the Sequencescape API.
+ # It uses the name and options provided to create the record.
+ #
+ # Example:
+ # tube = Tube.new(name: 'Sample Tube', options: { target: 'Tube', type: 'Sample' })
+ # tube.register!
+ #
+ class Tube < PurposeConfig
self.default_state_changer = 'StateChangers::TubeStateChanger'
self.default_options = {
@@ -92,7 +142,28 @@ def register!
end
end
- class Plate < PurposeConfig # rubocop:todo Style/Documentation
+ # The Plate class is a configuration class for defining the purpose and behavior of plates within the system.
+ # It inherits from PurposeConfig and sets default options specific to plates.
+ #
+ # Attributes:
+ # - default_options: A hash containing default configuration options for plates, including:
+ # - default_printer_type: The default printer type for plates.
+ # - presenter_class: The class responsible for presenting plates.
+ # - creator_class: The class responsible for creating plates from other labware.
+ # - label_class: The class responsible for labeling plates.
+ # - file_links: An array of hashes representing file links associated with plates, each containing:
+ # - name: The display name of the file link.
+ # - id: The identifier for the file link.
+ #
+ # Methods:
+ # - register!: Registers the plate purpose by creating a new PlatePurpose record in the Sequencescape API.
+ # It uses the name and options provided to create the record.
+ #
+ # Example:
+ # plate = Plate.new(name: 'Sample Plate', options: { target: 'Plate', type: 'Sample' })
+ # plate.register!
+ #
+ class Plate < PurposeConfig
self.default_options = {
default_printer_type: :plate_a,
presenter_class: 'Presenters::StandardPresenter',
diff --git a/spec/factories/purpose_config_factories.rb b/spec/factories/purpose_config_factories.rb
index 430cbafc3..32bc28a5f 100644
--- a/spec/factories/purpose_config_factories.rb
+++ b/spec/factories/purpose_config_factories.rb
@@ -253,18 +253,25 @@
# Configuration for a plate split to tube racks purpose
factory :plate_split_to_tube_racks_purpose_config do
+ asset_type { 'tube_rack' }
+ target { 'TubeRack' }
+ size { 96 }
+ type { 'TubeRack::Purpose' }
creator_class do
{
name: 'LabwareCreators::PlateSplitToTubeRacks',
args: {
- child_seq_tube_purpose_name: 'Seq Child Purpose',
+ child_seq_tube_purpose_name: 'SEQ Tube Purpose',
child_seq_tube_name_prefix: 'SEQ',
- child_spare_tube_purpose_name: 'Spare Child Purpose',
- child_spare_tube_name_prefix: 'SPR'
+ child_seq_tube_rack_purpose_name: 'SEQ TubeRack Purpose',
+ child_spare_tube_purpose_name: 'SPR Tube Purpose',
+ child_spare_tube_name_prefix: 'SPR',
+ child_spare_tube_rack_purpose_name: 'SPR TubeRack Purpose',
+ ancestor_stock_tube_purpose_name: 'Ancestor Tube Purpose'
}
}
end
- ancestor_stock_tube_purpose_name { 'Ancestor Tube Purpose' }
+ presenter_class { 'Presenters::TubeRackPresenter' }
end
# Configuration to set number_of_source_wells argument
diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb
index eff16df5f..0ebe07601 100644
--- a/spec/factories/request_factories.rb
+++ b/spec/factories/request_factories.rb
@@ -64,6 +64,8 @@
'id' => evaluator.order_id.to_s
}
}
+
+ request._cached_relationship(:request_type) { evaluator.request_type }
end
# Basic library creation request, such as wgs
diff --git a/spec/models/labware_creators/plate_split_to_tube_racks_spec.rb b/spec/models/labware_creators/plate_split_to_tube_racks_spec.rb
index 0e88bd6af..df52185b0 100644
--- a/spec/models/labware_creators/plate_split_to_tube_racks_spec.rb
+++ b/spec/models/labware_creators/plate_split_to_tube_racks_spec.rb
@@ -19,13 +19,27 @@
let(:user) { create :user }
let(:user_uuid) { user.uuid }
+
+ # child tube rack and tube details
+ let(:child_sequencing_tube_purpose_name) { 'SEQ Tube Purpose' }
let(:child_sequencing_tube_purpose_uuid) { SecureRandom.uuid }
- let(:child_sequencing_tube_purpose_name) { 'Seq Child Purpose' }
+ let(:child_sequencing_tube_rack_purpose_uuid) { SecureRandom.uuid }
+ let(:child_sequencing_tube_rack_purpose_name) { 'SEQ TubeRack Purpose' }
+ let(:child_sequencing_tube_rack_name) { 'SEQ Tube Rack' }
+ let(:child_sequencing_tube_rack_barcode) { 'TR00000001' }
+
+ let(:child_contingency_tube_purpose_name) { 'SPR Tube Purpose' }
let(:child_contingency_tube_purpose_uuid) { SecureRandom.uuid }
- let(:child_contingency_tube_purpose_name) { 'Spare Child Purpose' }
+ let(:child_contingency_tube_rack_purpose_uuid) { SecureRandom.uuid }
+ let(:child_contingency_tube_rack_purpose_name) { 'SPR TubeRack Purpose' }
+ let(:child_contingency_tube_rack_name) { 'SPR Tube Rack' }
+ let(:child_contingency_tube_rack_barcode) { 'TR00000002' }
+
+ # ancestor tube details
let(:ancestor_tube_purpose_uuid) { SecureRandom.uuid }
let(:ancestor_tube_purpose_name) { 'Ancestor Tube Purpose' }
+ # parent plate details
let(:parent_uuid) { SecureRandom.uuid }
# The parent plate needs to have several wells containing the same sample
@@ -108,14 +122,20 @@
)
end
+ let(:plate_includes) do
+ 'wells.aliquots,wells.aliquots.sample,wells.downstream_tubes,' \
+ 'wells.downstream_tubes.custom_metadatum_collection'
+ end
+
# parent plate v1 api
let(:parent_v1) { json :plate_with_metadata, uuid: parent_uuid, barcode_number: 6, qc_files_actions: %w[read create] }
# form attributes - required parameters for the labware creator
let(:form_attributes) do
- { user_uuid: user_uuid, purpose_uuid: child_sequencing_tube_purpose_uuid, parent_uuid: parent_uuid }
+ { user_uuid: user_uuid, purpose_uuid: child_sequencing_tube_rack_purpose_uuid, parent_uuid: parent_uuid }
end
+ # files
let(:sequencing_file) do
fixture_file_upload(
'spec/fixtures/files/scrna_core/scrna_core_sequencing_tube_rack_scan.csv',
@@ -130,69 +150,198 @@
)
end
- def prepare_created_child_tubes(tube_attributes)
- # Prepare child tubes and stub their lookups.
- child_tubes =
- tube_attributes.map { |attrs| create(:v2_tube, name: attrs[:name], foreign_barcode: attrs[:foreign_barcode]) }
- child_tubes.each { |child_tube| stub_v2_labware(child_tube) }
+ # coordinates of tubes in racks (matches files being uploaded)
+ let(:sequencing_file_coords) { %w[A1 B1] }
+ let(:contingency_file_coords) { %w[A1 B1 C1 E1 F1] }
- child_tubes
+ # tube racks
+ let(:sequencing_tube_rack) do
+ create(
+ :tube_rack,
+ name: child_sequencing_tube_rack_name,
+ labware_barcode: {
+ ean13_barcode: child_sequencing_tube_rack_barcode,
+ human_barcode: child_sequencing_tube_rack_barcode,
+ machine_barcode: child_sequencing_tube_rack_barcode
+ },
+ purpose_name: child_sequencing_tube_rack_purpose_name,
+ purpose_uuid: child_sequencing_tube_rack_purpose_uuid
+ )
end
- def expect_specific_tube_creation(child_purpose_uuid, child_tubes)
- # Create a mock for the specific tube creation.
- specific_tube_creation = double
- allow(specific_tube_creation).to receive(:children).and_return(child_tubes)
-
- # Expect the post request and return the mock.
- expect_api_v2_posts(
- 'SpecificTubeCreation',
- [
- {
- child_purpose_uuids: [child_purpose_uuid] * child_tubes.size,
- parent_uuids: [parent_uuid],
- tube_attributes: child_tubes.map { |tube| { name: tube.name, foreign_barcode: tube.foreign_barcode } },
- user_uuid: user_uuid
- }
- ],
- [specific_tube_creation]
+ let(:contingency_tube_rack) do
+ create(
+ :tube_rack,
+ name: child_contingency_tube_rack_name,
+ labware_barcode: {
+ ean13_barcode: child_contingency_tube_rack_barcode,
+ human_barcode: child_contingency_tube_rack_barcode,
+ machine_barcode: child_contingency_tube_rack_barcode
+ },
+ purpose_name: child_contingency_tube_rack_purpose_name,
+ purpose_uuid: child_contingency_tube_rack_purpose_uuid
)
end
- # tubes_hash should be a hash with tube rack barcodes as keys and arrays of tubes as values.
- def expect_custom_metadatum_collection_posts(tubes_hash)
- # Prepare the expected call arguments.
- expected_call_args =
- tubes_hash.flat_map do |tube_rack_barcode, tubes|
- tubes.map do |tube|
- {
- user_id: user.id,
- asset_id: tube.id,
- metadata: {
- tube_rack_barcode: tube_rack_barcode,
- tube_rack_position: tube.name.split(':').last
+ # Prepare child tubes and stub their lookups and those of their racked_tubes.
+ # tube_attributes should be an array of hashes with the tube name and foreign barcode.
+ # [
+ # { name: 'SPR:NT1O:A1', foreign_barcode: 'FX00000011' }
+ # etc...
+ # ]
+ # rubocop:disable Metrics/MethodLength
+ def prepare_created_child_tubes(tube_attributes, tube_rack)
+ tube_attributes.map do |tube_attrs|
+ tube_coordinate = tube_attrs[:name].split(':').last
+
+ # create the tube
+ child_tube =
+ create(
+ :v2_tube,
+ name: tube_attrs[:name],
+ purpose_uuid: tube_attrs[:purpose_uuid],
+ purpose_name: tube_attrs[:purpose_name],
+ barcode_prefix: 'FX',
+ barcode_number: tube_attrs[:barcode_number],
+ foreign_barcode: tube_attrs[:foreign_barcode]
+ )
+
+ # stub the tube
+ stub_v2_labware(child_tube)
+
+ # create the racked tube
+ racked_tube = create(:racked_tube, tube: child_tube, tube_rack: tube_rack, coordinate: tube_coordinate)
+
+ # stub the racked tube
+ stub_v2_racked_tube(racked_tube)
+
+ child_tube
+ end
+ end
+
+ # rubocop:enable Metrics/MethodLength
+
+ # Generate the attributes for the child tube racks.
+ # Example output
+ # [
+ # {
+ # :tube_rack_name=>"Seq Tube Rack",
+ # :tube_rack_barcode=>"TR00000001",
+ # :tube_rack_purpose_uuid=>"0ab4c9cc-4dad-11ef-8ca3-82c61098d1a1",
+ # :racked_tubes=>[
+ # {
+ # :tube_barcode=>"SQ45303801",
+ # :tube_name=>"SEQ:NT749R:A1",
+ # :tube_purpose_uuid=>"0ab4c9cc-4dad-11ef-8ca3-82c61098d1a1",
+ # :tube_position=>"A1",
+ # :parent_uuids=>["bd49e7f8-80a1-11ef-bab6-82c61098d1a0"]
+ # },
+ # etc... more tubes
+ # ]
+ # },
+ # etc... second rack for contingency tubes
+ # ]
+ # Example input
+ # params = {
+ # sequencing_tubes: [ array of v2_tube objects ],
+ # sequencing_tube_parent_well_uuids: [array of parent well uuids],
+ # contingency_tubes: [array of v2 tubes],
+ # contingency_tube_parent_well_uuids: [array of parent well uuids],
+ # }
+ # rubocop:disable Metrics/AbcSize
+ def generate_child_tube_rack_attributes(params)
+ tr_attributes = []
+ if params[:sequencing_tubes].present?
+ tr_attributes << {
+ tube_rack_name: child_sequencing_tube_rack_name,
+ tube_rack_barcode: sequencing_tube_rack.labware_barcode.human,
+ tube_rack_purpose_uuid: child_sequencing_tube_rack_purpose_uuid,
+ racked_tubes:
+ params[:sequencing_tubes].each_with_index.map do |tube, tube_index|
+ {
+ tube_barcode: tube.foreign_barcode,
+ tube_name: tube.name,
+ tube_purpose_uuid: tube.purpose.uuid,
+ tube_position: tube.name.split(':').last,
+ parent_uuids: [params[:sequencing_tube_parent_well_uuids][tube_index]]
}
- }
- end
- end
+ end
+ }
+ end
+
+ if params[:contingency_tubes].present?
+ tr_attributes << {
+ tube_rack_name: child_contingency_tube_rack_name,
+ tube_rack_barcode: contingency_tube_rack.labware_barcode.human,
+ tube_rack_purpose_uuid: child_contingency_tube_rack_purpose_uuid,
+ racked_tubes:
+ params[:contingency_tubes].each_with_index.map do |tube, tube_index|
+ {
+ tube_barcode: tube.foreign_barcode,
+ tube_name: tube.name,
+ tube_purpose_uuid: tube.purpose.uuid,
+ tube_position: tube.name.split(':').last,
+ parent_uuids: [params[:contingency_tube_parent_well_uuids][tube_index]]
+ }
+ end
+ }
+ end
- # Expect the post requests.
- expect_api_v2_posts('CustomMetadatumCollection', expected_call_args)
+ tr_attributes
+ end
+
+ # rubocop:enable Metrics/AbcSize
+
+ # {
+ # : ,
+ # etc.
+ # }
+ def generate_tube_uuids_by_barcode
+ (sequencing_tubes + contingency_tubes).each_with_object({}) { |tube, hash| hash[tube.foreign_barcode] = tube.uuid }
+ end
+
+ # Endpoint returns child tube rack objects
+ def expect_specific_tube_rack_creation(child_tube_racks, child_tube_rack_attributes)
+ # set up method override to get created child tube uuids by barcode
+ allow(subject).to receive(:tube_uuids_by_barcode).and_return(generate_tube_uuids_by_barcode)
+
+ # Create a mock for the specific tube rack creation in Sequencescape.
+ specific_tube_rack_creation = double
+ allow(specific_tube_rack_creation).to receive(:children).and_return(child_tube_racks)
+
+ # Expect the post request and return the mock.
+ expect_api_v2_posts(
+ 'SpecificTubeRackCreation',
+ [{ parent_uuids: [parent_uuid], tube_rack_attributes: child_tube_rack_attributes, user_uuid: user_uuid }],
+ [specific_tube_rack_creation]
+ )
end
before do
- # need both child tubes to have a purpose config here
+ # set up the child tube rack purpose configs in the Settings
create(
:plate_split_to_tube_racks_purpose_config,
- name: child_sequencing_tube_purpose_name,
- uuid: child_sequencing_tube_purpose_uuid
+ name: child_sequencing_tube_rack_purpose_name,
+ uuid: child_sequencing_tube_rack_purpose_uuid
)
create(
:plate_split_to_tube_racks_purpose_config,
- name: child_contingency_tube_purpose_name,
- uuid: child_contingency_tube_purpose_uuid
+ name: child_contingency_tube_rack_purpose_name,
+ uuid: child_contingency_tube_rack_purpose_uuid
)
+ # stub the tube rack purposes
+ stub_v2_tube_rack_purpose(sequencing_tube_rack.purpose)
+ stub_v2_tube_rack_purpose(contingency_tube_rack.purpose)
+
+ # stub the child tube racks
+ stub_v2_labware(sequencing_tube_rack)
+ stub_v2_labware(contingency_tube_rack)
+
+ # set up the child tube purposes
+ create(:purpose_config, name: child_sequencing_tube_purpose_name, uuid: child_sequencing_tube_purpose_uuid)
+ create(:purpose_config, name: child_contingency_tube_purpose_name, uuid: child_contingency_tube_purpose_uuid)
+
# ancestor tube purpose config
create(:purpose_config, name: ancestor_tube_purpose_name, uuid: ancestor_tube_purpose_uuid)
@@ -216,6 +365,101 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
end
end
+ describe '#create_labware' do
+ context 'when child_tube_racks is blank' do
+ before { allow(subject).to receive(:create_child_tubes_and_racks).and_return([]) }
+
+ it 'adds an error and returns false' do
+ result = subject.create_labware!
+
+ expect(result).to be_falsey
+ expect(subject.errors[:base]).to include(
+ 'Failed to create child tube racks and tubes, nothing returned from API creation call'
+ )
+ end
+ end
+ end
+
+ describe '#redirection_target' do
+ before { stub_v2_plate(parent_plate, stub_search: false, custom_includes: plate_includes) }
+
+ it 'returns the parent object' do
+ expect(subject.redirection_target).to eq(parent_plate)
+ end
+ end
+
+ describe '#anchor' do
+ it 'returns the string "relatives_tab"' do
+ expect(subject.anchor).to eq('relatives_tab')
+ end
+ end
+
+ describe '#generate_tube_uuids_by_barcode' do
+ let(:tube1) { double('Tube', barcode: double('Barcode', human: 'barcode1'), uuid: 'uuid1') }
+ let(:tube2) { double('Tube', barcode: double('Barcode', human: 'barcode2'), uuid: 'uuid2') }
+ let(:racked_tube1) { double('RackedTube', tube: tube1) }
+ let(:racked_tube2) { double('RackedTube', tube: tube2) }
+ let(:tube_rack) { double('TubeRack', racked_tubes: [racked_tube1, racked_tube2]) }
+ let(:child_tube_racks) { { 'rack1' => tube_rack } }
+
+ before { allow(subject).to receive(:child_tube_racks).and_return(child_tube_racks) }
+
+ it 'generates a hash mapping tube barcodes to their UUIDs' do
+ result = subject.send(:generate_tube_uuids_by_barcode)
+
+ expected_result = { 'barcode1' => 'uuid1', 'barcode2' => 'uuid2' }
+
+ expect(result).to eq(expected_result)
+ end
+ end
+
+ describe '#validate_tube_barcodes_for_well!' do
+ let(:well) { double('Well', position: { name: 'A1' }) }
+
+ context 'when tube_barcodes_for_well is present' do
+ it 'does not raise an error' do
+ tube_barcodes_for_well = %w[barcode1 barcode2]
+ expect { subject.send(:validate_tube_barcodes_for_well!, tube_barcodes_for_well, well) }.not_to raise_error
+ end
+ end
+
+ context 'when tube_barcodes_for_well is not present' do
+ it 'raises an error' do
+ tube_barcodes_for_well = nil
+ expect { subject.send(:validate_tube_barcodes_for_well!, tube_barcodes_for_well, well) }.to raise_error(
+ RuntimeError,
+ "Unable to identify the child tube barcodes for parent well 'A1'"
+ )
+ end
+ end
+ end
+
+ describe '#fetch_tube_uuid_for_barcode' do
+ let(:well) { double('Well', position: { name: 'A1' }) }
+
+ before do
+ allow(subject).to receive(:tube_uuids_by_barcode).and_return({ 'barcode1' => 'uuid1', 'barcode2' => 'uuid2' })
+ end
+
+ context 'when the tube UUID is found' do
+ it 'returns the tube UUID' do
+ tube_barcode_for_well = 'barcode1'
+ result = subject.send(:fetch_tube_uuid_for_barcode, tube_barcode_for_well, well)
+ expect(result).to eq('uuid1')
+ end
+ end
+
+ context 'when the tube UUID is not found' do
+ it 'raises an error' do
+ tube_barcode_for_well = 'unknown_barcode'
+ expect { subject.send(:fetch_tube_uuid_for_barcode, tube_barcode_for_well, well) }.to raise_error(
+ RuntimeError,
+ "Unable to identify the newly created child tube for parent well 'A1'"
+ )
+ end
+ end
+ end
+
context '#must_have_correct_number_of_tubes_in_rack_files' do
let(:num_parent_wells) { 96 }
let(:num_parent_unique_samples) { 48 }
@@ -223,13 +467,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
let(:num_contingency_tubes) { 48 }
before do
- stub_v2_plate(
- parent_plate,
- stub_search: false,
- custom_includes:
- 'wells.aliquots,wells.aliquots.sample,wells.downstream_tubes,' \
- 'wells.downstream_tubes.custom_metadatum_collection'
- )
+ stub_v2_plate(parent_plate, stub_search: false, custom_includes: plate_includes)
allow(subject).to receive(:num_sequencing_tubes).and_return(num_sequencing_tubes)
allow(subject).to receive(:num_contingency_tubes).and_return(num_contingency_tubes)
allow(subject).to receive(:num_parent_wells).and_return(num_parent_wells)
@@ -250,7 +488,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
let(:form_attributes) do
{
user_uuid: user_uuid,
- purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_uuid: child_contingency_tube_rack_purpose_uuid,
parent_uuid: parent_uuid,
sequencing_file: sequencing_file
}
@@ -293,7 +531,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
let(:form_attributes) do
{
user_uuid: user_uuid,
- purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_uuid: child_contingency_tube_rack_purpose_uuid,
parent_uuid: parent_uuid,
sequencing_file: sequencing_file,
contingency_file: contingency_file
@@ -346,15 +584,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
end
context '#check_tube_rack_barcodes_differ_between_files' do
- before do
- stub_v2_plate(
- parent_plate,
- stub_search: false,
- custom_includes:
- 'wells.aliquots,wells.aliquots.sample,wells.downstream_tubes,' \
- 'wells.downstream_tubes.custom_metadatum_collection'
- )
- end
+ before { stub_v2_plate(parent_plate, stub_search: false, custom_includes: plate_includes) }
context 'when files are not present' do
before { subject.validate }
@@ -370,7 +600,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
let(:form_attributes) do
{
user_uuid: user_uuid,
- purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_uuid: child_contingency_tube_rack_purpose_uuid,
parent_uuid: parent_uuid,
sequencing_file: sequencing_file,
contingency_file: contingency_file
@@ -399,7 +629,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
let(:form_attributes) do
{
user_uuid: user_uuid,
- purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_uuid: child_contingency_tube_rack_purpose_uuid,
parent_uuid: parent_uuid,
sequencing_file: contingency_file,
contingency_file: contingency_file
@@ -431,7 +661,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
let(:form_attributes) do
{
user_uuid: user_uuid,
- purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_uuid: child_contingency_tube_rack_purpose_uuid,
parent_uuid: parent_uuid,
sequencing_file: sequencing_file,
contingency_file: contingency_file
@@ -464,15 +694,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
end
context '#check_tube_barcodes_differ_between_files' do
- before do
- stub_v2_plate(
- parent_plate,
- stub_search: false,
- custom_includes:
- 'wells.aliquots,wells.aliquots.sample,wells.downstream_tubes,' \
- 'wells.downstream_tubes.custom_metadatum_collection'
- )
- end
+ before { stub_v2_plate(parent_plate, stub_search: false, custom_includes: plate_includes) }
context 'when files are not present' do
before { subject.validate }
@@ -488,7 +710,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
let(:form_attributes) do
{
user_uuid: user_uuid,
- purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_uuid: child_contingency_tube_rack_purpose_uuid,
parent_uuid: parent_uuid,
sequencing_file: sequencing_file,
contingency_file: contingency_file
@@ -524,7 +746,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
let(:form_attributes) do
{
user_uuid: user_uuid,
- purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_uuid: child_contingency_tube_rack_purpose_uuid,
parent_uuid: parent_uuid,
sequencing_file: sequencing_file,
contingency_file: contingency_file
@@ -533,19 +755,19 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
let(:seq_tube_details) do
{
'A1' => {
- 'tube_rack_barcode' => 'TR00000001',
+ 'tube_rack_barcode' => child_sequencing_tube_rack_barcode,
'tube_barcode' => 'FX00000001'
},
'B1' => {
- 'tube_rack_barcode' => 'TR00000001',
+ 'tube_rack_barcode' => child_sequencing_tube_rack_barcode,
'tube_barcode' => 'FX00000002'
},
'C1' => {
- 'tube_rack_barcode' => 'TR00000001',
+ 'tube_rack_barcode' => child_sequencing_tube_rack_barcode,
'tube_barcode' => 'FX00000011'
},
'D1' => {
- 'tube_rack_barcode' => 'TR00000001',
+ 'tube_rack_barcode' => child_sequencing_tube_rack_barcode,
'tube_barcode' => 'FX00000012'
}
}
@@ -569,7 +791,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
let(:tube_rack_file) { double('tube_rack_file') } # don't need an actual file for this test
let(:tube_posn) { 'A1' }
let(:foreign_barcode) { '123456' }
- let(:tube_rack_barcode) { 'TR00000001' }
+ let(:tube_rack_barcode) { child_sequencing_tube_rack_barcode }
let(:tube_details) { { 'tube_barcode' => foreign_barcode, 'tube_rack_barcode' => tube_rack_barcode } }
let(:msg_prefix) { 'Sequencing' }
let(:existing_tube) { create(:v2_tube, state: 'passed', barcode_number: 1, foreign_barcode: foreign_barcode) }
@@ -626,27 +848,41 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
)
end
+ # create the contingency tubes
let(:contingency_tubes) do
prepare_created_child_tubes(
[
# sample 1 from well A2 to contingency tube 1 in A1
- { name: 'SPR:NT1O:A1', foreign_barcode: 'FX00000011' },
+ {
+ name: 'SPR:NT1O:A1',
+ foreign_barcode: 'FX00000011',
+ barcode_number: 11,
+ purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_name: child_contingency_tube_purpose_name
+ },
# sample 2 from well B2 to contingency tube 2 in B1
- { name: 'SPR:NT2P:B1', foreign_barcode: 'FX00000012' },
+ {
+ name: 'SPR:NT2P:B1',
+ foreign_barcode: 'FX00000012',
+ barcode_number: 12,
+ purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_name: child_contingency_tube_purpose_name
+ },
# sample 1 from well A3 to contingency tube 3 in C1
- { name: 'SPR:NT1O:C1', foreign_barcode: 'FX00000013' }
- ]
+ {
+ name: 'SPR:NT1O:C1',
+ foreign_barcode: 'FX00000013',
+ barcode_number: 13,
+ purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_name: child_contingency_tube_purpose_name
+ }
+ ],
+ contingency_tube_rack
)
end
before do
- stub_v2_plate(
- parent_plate,
- stub_search: false,
- custom_includes:
- 'wells.aliquots,wells.aliquots.sample,wells.downstream_tubes,' \
- 'wells.downstream_tubes.custom_metadatum_collection'
- )
+ stub_v2_plate(parent_plate, stub_search: false, custom_includes: plate_includes)
stub_api_get(parent_uuid, body: parent_v1)
end
@@ -654,7 +890,7 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
let(:form_attributes) do
{
user_uuid: user_uuid,
- purpose_uuid: child_sequencing_tube_purpose_uuid,
+ purpose_uuid: child_sequencing_tube_rack_purpose_uuid,
parent_uuid: parent_uuid,
sequencing_file: sequencing_file,
contingency_file: contingency_file
@@ -692,14 +928,28 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
content
end
+ # create the sequencing tubes
let(:sequencing_tubes) do
prepare_created_child_tubes(
[
# sample 1 in well A1 to seq tube 1 in A1
- { name: 'SEQ:NT1O:A1', foreign_barcode: 'FX00000001' },
+ {
+ name: 'SEQ:NT1O:A1',
+ foreign_barcode: 'FX00000001',
+ barcode_number: 1,
+ purpose_uuid: child_sequencing_tube_purpose_uuid,
+ purpose_name: child_sequencing_tube_purpose_name
+ },
# sample 2 in well B1 to seq tube 2 in B1
- { name: 'SEQ:NT2P:B1', foreign_barcode: 'FX00000002' }
- ]
+ {
+ name: 'SEQ:NT2P:B1',
+ foreign_barcode: 'FX00000002',
+ barcode_number: 2,
+ purpose_uuid: child_sequencing_tube_purpose_uuid,
+ purpose_name: child_sequencing_tube_purpose_name
+ }
+ ],
+ sequencing_tube_rack
)
end
@@ -750,12 +1000,32 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
before { stub_v2_user(user) }
it 'creates the child tubes' do
- expect_specific_tube_creation(child_sequencing_tube_purpose_uuid, sequencing_tubes)
- expect_specific_tube_creation(child_contingency_tube_purpose_uuid, contingency_tubes)
+ child_tube_racks = [sequencing_tube_rack, contingency_tube_rack]
- expect_custom_metadatum_collection_posts(
- { 'TR00000001' => sequencing_tubes, 'TR00000002' => contingency_tubes }
- )
+ sequencing_tube_parent_well_uuids = [
+ parent_plate.well_at_location('A1').uuid,
+ parent_plate.well_at_location('B1').uuid
+ ]
+ contingency_tube_parent_well_uuids = [
+ parent_plate.well_at_location('A2').uuid,
+ parent_plate.well_at_location('B2').uuid,
+ parent_plate.well_at_location('A3').uuid
+ ]
+
+ params = {
+ sequencing_tubes:,
+ sequencing_tube_parent_well_uuids:,
+ contingency_tubes:,
+ contingency_tube_parent_well_uuids:
+ }
+ child_tube_rack_attributes = generate_child_tube_rack_attributes(params)
+
+ expect_specific_tube_rack_creation(child_tube_racks, child_tube_rack_attributes)
+
+ # expect_custom_metadatum_collection_posts(
+ # { 'TR00000001' => sequencing_tubes, 'TR00000002' => contingency_tubes }
+ # )
+ # TODO: check racked tubes? done on SS side so would be mocked anyway
expect(subject.valid?).to be_truthy
expect(subject.save).to be_truthy
@@ -797,35 +1067,77 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
)
end
+ # create the sequencing tubes
let(:sequencing_tubes) do
prepare_created_child_tubes(
[
# sample 2 in well B1 to seq tube 1 in A1
- { name: 'SEQ:NT2P:A1', foreign_barcode: 'FX00000001' },
+ {
+ name: 'SEQ:NT2P:A1',
+ foreign_barcode: 'FX00000001',
+ barcode_number: 1,
+ purpose_uuid: child_sequencing_tube_purpose_uuid,
+ purpose_name: child_sequencing_tube_purpose_name
+ },
# sample 1 in well A2 to seq tube 2 in B1
- { name: 'SEQ:NT1O:B1', foreign_barcode: 'FX00000002' }
- ]
+ {
+ name: 'SEQ:NT1O:B1',
+ foreign_barcode: 'FX00000002',
+ barcode_number: 2,
+ purpose_uuid: child_sequencing_tube_purpose_uuid,
+ purpose_name: child_sequencing_tube_purpose_name
+ }
+ ],
+ sequencing_tube_rack
)
end
+ # create the contingency tubes
let(:contingency_tubes) do
prepare_created_child_tubes(
[
# sample 2 from well B2 to contingency tube 1 in A1
- { name: 'SPR:NT2P:A1', foreign_barcode: 'FX00000011' },
+ {
+ name: 'SPR:NT2P:A1',
+ foreign_barcode: 'FX00000011',
+ barcode_number: 11,
+ purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_name: child_contingency_tube_purpose_name
+ },
# sample 1 from well A3 to contingency tube 2 in B1
- { name: 'SPR:NT1O:B1', foreign_barcode: 'FX00000012' }
- ]
+ {
+ name: 'SPR:NT1O:B1',
+ foreign_barcode: 'FX00000012',
+ barcode_number: 12,
+ purpose_uuid: child_contingency_tube_purpose_uuid,
+ purpose_name: child_contingency_tube_purpose_name
+ }
+ ],
+ contingency_tube_rack
)
end
it 'does not create a tube for the failed well' do
- expect_specific_tube_creation(child_sequencing_tube_purpose_uuid, sequencing_tubes)
- expect_specific_tube_creation(child_contingency_tube_purpose_uuid, contingency_tubes)
+ child_tube_racks = [sequencing_tube_rack, contingency_tube_rack]
- expect_custom_metadatum_collection_posts(
- { 'TR00000001' => sequencing_tubes, 'TR00000002' => contingency_tubes }
- )
+ sequencing_tube_parent_well_uuids = [
+ parent_plate.well_at_location('B1').uuid,
+ parent_plate.well_at_location('A2').uuid
+ ]
+ contingency_tube_parent_well_uuids = [
+ parent_plate.well_at_location('B2').uuid,
+ parent_plate.well_at_location('A3').uuid
+ ]
+
+ params = {
+ sequencing_tubes:,
+ sequencing_tube_parent_well_uuids:,
+ contingency_tubes:,
+ contingency_tube_parent_well_uuids:
+ }
+ child_tube_rack_attributes = generate_child_tube_rack_attributes(params)
+
+ expect_specific_tube_rack_creation(child_tube_racks, child_tube_rack_attributes)
expect(subject.valid?).to be_truthy
expect(subject.save).to be_truthy
@@ -836,11 +1148,23 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
end
end
- context 'with just a sequencing file' do
+ # This test is to check that the correct tube rack and tubes are created when only a sequencing file is provided.
+ # NB. The parent plant must have ONLY unique samples in it. No duplicates.
+ context 'with just a sequencing file and unique samples' do
+ let(:parent_plate) do
+ create(
+ :v2_plate,
+ uuid: parent_uuid,
+ wells: [parent_well_a1, parent_well_b1],
+ barcode_number: 6,
+ ancestors: ancestor_tubes
+ )
+ end
+
let(:form_attributes) do
{
user_uuid: user_uuid,
- purpose_uuid: child_sequencing_tube_purpose_uuid,
+ purpose_uuid: child_sequencing_tube_rack_purpose_uuid,
parent_uuid: parent_uuid,
sequencing_file: sequencing_file
}
@@ -870,14 +1194,28 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
)
end
+ # create the sequencing tubes
let(:sequencing_tubes) do
prepare_created_child_tubes(
[
# sample 1 from well A1 to sequencing tube 1 in A1
- { name: 'SEQ:NT1O:A1', foreign_barcode: 'FX00000001' },
+ {
+ name: 'SEQ:NT1O:A1',
+ foreign_barcode: 'FX00000001',
+ barcode_number: 1,
+ purpose_uuid: child_sequencing_tube_purpose_uuid,
+ purpose_name: child_sequencing_tube_purpose_name
+ },
# sample 2 from well B1 to sequencing tube 2 in B1
- { name: 'SEQ:NT2P:B1', foreign_barcode: 'FX00000002' }
- ]
+ {
+ name: 'SEQ:NT2P:B1',
+ foreign_barcode: 'FX00000002',
+ barcode_number: 2,
+ purpose_uuid: child_sequencing_tube_purpose_uuid,
+ purpose_name: child_sequencing_tube_purpose_name
+ }
+ ],
+ sequencing_tube_rack
)
end
@@ -907,9 +1245,22 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
before { stub_v2_user(user) }
it 'creates the child tubes' do
- # Contingency tubes creation
- expect_specific_tube_creation(child_sequencing_tube_purpose_uuid, sequencing_tubes)
- expect_custom_metadatum_collection_posts({ 'TR00000001' => sequencing_tubes })
+ child_tube_racks = [sequencing_tube_rack]
+
+ sequencing_tube_parent_well_uuids = [
+ parent_plate.well_at_location('A1').uuid,
+ parent_plate.well_at_location('B1').uuid
+ ]
+
+ params = {
+ sequencing_tubes: sequencing_tubes,
+ sequencing_tube_parent_well_uuids: sequencing_tube_parent_well_uuids,
+ contingency_tubes: nil,
+ contingency_tube_parent_well_uuids: nil
+ }
+ child_tube_rack_attributes = generate_child_tube_rack_attributes(params)
+
+ expect_specific_tube_rack_creation(child_tube_racks, child_tube_rack_attributes)
expect(subject.valid?).to be_truthy
expect(subject.save).to be_truthy
@@ -917,5 +1268,11 @@ def expect_custom_metadatum_collection_posts(tubes_hash)
expect(stub_transfer_creation_request).to have_been_made.once
end
end
+
+ # This test is to check it is not valid to have duplicate samples in the parent plate whilst only providing a
+ # sequencing file.
+ # context 'with just a sequencing file and duplicate samples' do
+ # # TODO: add validation in labware creator and test for duplicate samples
+ # end
end
end
diff --git a/spec/models/state_changers_spec.rb b/spec/models/state_changers_spec.rb
index 702c30543..d48bbb8c0 100644
--- a/spec/models/state_changers_spec.rb
+++ b/spec/models/state_changers_spec.rb
@@ -5,14 +5,14 @@
RSpec.describe StateChangers::DefaultStateChanger do
has_a_working_api
- let(:plate_uuid) { SecureRandom.uuid }
- let(:plate) { json :plate, uuid: plate_uuid, state: plate_state }
+ let(:labware_uuid) { SecureRandom.uuid }
+ let(:plate) { json :plate, uuid: labware_uuid, state: plate_state }
let(:well_collection) { json :well_collection, default_state: plate_state, custom_state: failed_wells }
let(:failed_wells) { {} }
let(:user_uuid) { SecureRandom.uuid }
let(:reason) { 'Because I want to' }
let(:customer_accepts_responsibility) { false }
- subject { StateChangers::DefaultStateChanger.new(api, plate_uuid, user_uuid) }
+ subject { StateChangers::DefaultStateChanger.new(api, labware_uuid, user_uuid) }
describe '#move_to!' do
before do
@@ -20,11 +20,11 @@
'StateChange',
[
{
- contents: wells_to_pass,
+ contents: coordinates_to_pass,
customer_accepts_responsibility: customer_accepts_responsibility,
reason: reason,
target_state: target_state,
- target_uuid: plate_uuid,
+ target_uuid: labware_uuid,
user_uuid: user_uuid
}
]
@@ -40,7 +40,7 @@
context 'on a fully pending plate' do
let(:plate_state) { 'pending' }
let(:target_state) { 'passed' }
- let(:wells_to_pass) { nil }
+ let(:coordinates_to_pass) { nil }
it_behaves_like 'a state changer'
end
@@ -48,12 +48,12 @@
# Ideally we wouldn't need this query here, but we don't know that
# until we perform it.
before do
- stub_api_get(plate_uuid, body: plate)
- stub_api_get(plate_uuid, 'wells', body: well_collection)
+ stub_api_get(labware_uuid, body: plate)
+ stub_api_get(labware_uuid, 'wells', body: well_collection)
end
# if no wells are failed we leave contents blank and state changer assumes full plate
- let(:wells_to_pass) { nil }
+ let(:coordinates_to_pass) { nil }
let(:plate_state) { 'passed' }
let(:target_state) { 'qc_complete' }
@@ -68,11 +68,11 @@
# when some wells are failed we filter those out of the contents
let(:failed_wells) { { 'A1' => 'failed', 'D1' => 'failed' } }
- let(:wells_to_pass) { WellHelpers.column_order - failed_wells.keys }
+ let(:coordinates_to_pass) { WellHelpers.column_order - failed_wells.keys }
before do
- stub_api_get(plate_uuid, body: plate)
- stub_api_get(plate_uuid, 'wells', body: well_collection)
+ stub_api_get(labware_uuid, body: plate)
+ stub_api_get(labware_uuid, 'wells', body: well_collection)
end
it_behaves_like 'a state changer'
@@ -80,21 +80,21 @@
context 'on use of an automated plate state changer' do
let(:plate_state) { 'pending' }
- let!(:plate) { create :v2_plate_for_aggregation, uuid: plate_uuid, state: plate_state }
+ let!(:plate) { create :v2_plate_for_aggregation, uuid: labware_uuid, state: plate_state }
let(:target_state) { 'passed' }
- let(:wells_to_pass) { nil }
+ let(:coordinates_to_pass) { nil }
let(:plate_purpose_name) { 'Limber Bespoke Aggregation' }
let(:work_completion_request) do
- { 'work_completion' => { target: plate_uuid, submissions: %w[pool-1-uuid pool-2-uuid], user: user_uuid } }
+ { 'work_completion' => { target: labware_uuid, submissions: %w[pool-1-uuid pool-2-uuid], user: user_uuid } }
end
let(:work_completion) { json :work_completion }
let!(:work_completion_creation) do
stub_api_post('work_completions', payload: work_completion_request, body: work_completion)
end
- subject { StateChangers::AutomaticPlateStateChanger.new(api, plate_uuid, user_uuid) }
+ subject { StateChangers::AutomaticPlateStateChanger.new(api, labware_uuid, user_uuid) }
- before { stub_v2_plate(plate, stub_search: false, custom_query: [:plate_for_completion, plate_uuid]) }
+ before { stub_v2_plate(plate, stub_search: false, custom_query: [:plate_for_completion, labware_uuid]) }
context 'when config request type matches in progress submissions' do
before { create :aggregation_purpose_config, uuid: plate.purpose.uuid, name: plate_purpose_name }
@@ -139,5 +139,64 @@
end
end
end
+
+ context 'on use of a tube rack state changer' do
+ let(:tube_starting_state) { 'pending' }
+ let(:tube_failed_state) { 'failed' }
+
+ let(:target_state) { 'qc_complete' }
+
+ let(:tube1_uuid) { SecureRandom.uuid }
+ let(:tube2_uuid) { SecureRandom.uuid }
+ let(:tube3_uuid) { SecureRandom.uuid }
+
+ let(:tube1) { create :v2_tube, uuid: tube1_uuid, state: tube_failed_state, barcode_number: 1 }
+ let(:tube2) { create :v2_tube, uuid: tube2_uuid, state: tube_starting_state, barcode_number: 2 }
+ let(:tube3) { create :v2_tube, uuid: tube3_uuid, state: tube_starting_state, barcode_number: 3 }
+
+ let!(:tube_rack) { create :tube_rack, barcode_number: 4, uuid: labware_uuid }
+
+ let(:racked_tube1) { create :racked_tube, coordinate: 'A1', tube: tube1, tube_rack: tube_rack }
+ let(:racked_tube2) { create :racked_tube, coordinate: 'B1', tube: tube2, tube_rack: tube_rack }
+ let(:racked_tube3) { create :racked_tube, coordinate: 'C1', tube: tube3, tube_rack: tube_rack }
+
+ let(:labware) { tube_rack }
+
+ subject { StateChangers::TubeRackStateChanger.new(api, labware_uuid, user_uuid) }
+
+ before do
+ stub_v2_tube_rack(tube_rack)
+
+ # allow(labware).to receive(:racked_tubes).and_return([racked_tube1, racked_tube2, racked_tube3])
+ end
+
+ context 'when all tubes are in failed state' do
+ let(:coordinates_to_pass) { [] }
+
+ before do
+ # stub_v2_tube_rack(tube_rack)
+ allow(labware).to receive(:racked_tubes).and_return([racked_tube1])
+ end
+
+ # if all the tubes are already in the target state expect contents to be empty
+ # TODO: I'm not sure this is correct behaviour, it should probably raise an error
+ # or a validation should catch that the state change is not needed
+ it 'returns empty array' do
+ expect(subject.contents_for(target_state)).to eq([])
+ subject.move_to!(target_state, reason, customer_accepts_responsibility)
+ end
+ end
+
+ context 'when some tubes are not in failed state' do
+ let(:coordinates_to_pass) { %w[B1 C1] }
+
+ before { allow(labware).to receive(:racked_tubes).and_return([racked_tube1, racked_tube2, racked_tube3]) }
+
+ it 'returns the coordinates of tubes not in failed state' do
+ expect(subject.contents_for(target_state)).to eq(%w[B1 C1])
+ subject.move_to!(target_state, reason, customer_accepts_responsibility)
+ end
+ end
+ end
end
end
diff --git a/spec/sequencescape/api/v2/plate_spec.rb b/spec/sequencescape/api/v2/plate_spec.rb
index b68382890..024b890ab 100644
--- a/spec/sequencescape/api/v2/plate_spec.rb
+++ b/spec/sequencescape/api/v2/plate_spec.rb
@@ -9,6 +9,7 @@
it { is_expected.to be_plate }
it { is_expected.to_not be_tube }
+ it { is_expected.to_not be_tube_rack }
describe '#stock_plate' do
let(:stock_plates) { create_list :v2_stock_plate, 2 }
diff --git a/spec/sequencescape/api/v2/tube_rack_spec.rb b/spec/sequencescape/api/v2/tube_rack_spec.rb
new file mode 100644
index 000000000..96b750fa7
--- /dev/null
+++ b/spec/sequencescape/api/v2/tube_rack_spec.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Sequencescape::Api::V2::TubeRack, type: :model do
+ describe '#requests_in_progress' do
+ let!(:tube_rack) { create(:tube_rack, tubes: { A1: tube1, B1: tube2 }) }
+
+ context 'when there are no requests' do
+ let(:tube1) { create(:v2_tube, aliquots: [create(:v2_aliquot, request: nil)]) }
+ let(:tube2) { create(:v2_tube, aliquots: [create(:v2_aliquot, request: nil)]) }
+
+ it 'returns an empty array' do
+ expect(tube_rack.requests_in_progress).to eq([])
+ end
+ end
+
+ context 'when there are requests on the tube aliquots' do
+ let(:request_type1) { create(:request_type, key: 'type1') }
+ let(:request_type2) { create(:request_type, key: 'type2') }
+ let(:request1) { create(:request, request_type: request_type1, state: 'started') }
+ let(:request2) { create(:request, request_type: request_type2, state: 'started') }
+ let(:tube1) { create(:v2_tube, aliquots: [create(:v2_aliquot, request: request1)]) }
+ let(:tube2) { create(:v2_tube, aliquots: [create(:v2_aliquot, request: request2)]) }
+
+ before do
+ tube1
+ tube2
+ end
+
+ it 'returns all requests in progress' do
+ expect(tube_rack.requests_in_progress.map(&:id)).to match_array([request1.id, request2.id])
+ end
+
+ it 'filters requests by request types to complete' do
+ expect(tube_rack.requests_in_progress(request_types_to_complete: 'type1').map(&:id)).to match_array(
+ [request1.id]
+ )
+ expect(tube_rack.requests_in_progress(request_types_to_complete: 'type2').map(&:id)).to match_array(
+ [request2.id]
+ )
+ end
+ end
+ end
+
+ describe '#all_requests' do
+ let!(:tube_rack) { create(:tube_rack, tubes: { A1: tube1, B1: tube2 }) }
+
+ context 'when there are no requests' do
+ let(:tube1) { create(:v2_tube, aliquots: [create(:v2_aliquot, request: nil)]) }
+ let(:tube2) { create(:v2_tube, aliquots: [create(:v2_aliquot, request: nil)]) }
+
+ it 'returns an empty array' do
+ expect(tube_rack.all_requests).to eq([])
+ end
+ end
+
+ context 'when there are requests' do
+ let(:request_type1) { create(:request_type, key: 'type1') }
+ let(:request_type2) { create(:request_type, key: 'type2') }
+ let(:request1) { create(:request, request_type: request_type1, state: 'started') }
+ let(:request2) { create(:request, request_type: request_type2, state: 'started') }
+
+ # set up tube 1 with a request as source
+ let(:receptacle1) do
+ create(
+ :v2_receptacle,
+ qc_results: [],
+ aliquots: [create(:v2_aliquot, request: nil)],
+ requests_as_source: [request1]
+ )
+ end
+ let(:tube1) { create(:v2_tube, receptacle: receptacle1) }
+
+ # set up tube 2 with an aliquot request
+ let(:tube2) { create(:v2_tube, aliquots: [create(:v2_aliquot, request: request2)]) }
+
+ before do
+ tube1
+ tube2
+ end
+
+ it 'returns all requests associated with the tube rack' do
+ expect(tube_rack.all_requests.map(&:id)).to match_array([request1.id, request2.id])
+ end
+ end
+ end
+
+ describe '#to_param' do
+ it 'returns the uuid of the TubeRack' do
+ uuid = '123e4567-e89b-12d3-a456-426614174000'
+ tube_rack = described_class.new(uuid:)
+
+ expect(tube_rack.to_param).to eq(uuid)
+ end
+ end
+
+ describe '#model_name' do
+ it 'returns an instance of ActiveModel::Name with the correct parameters' do
+ tube_rack = described_class.new
+ model_name = tube_rack.model_name
+
+ expect(model_name).to be_an_instance_of(ActiveModel::Name)
+ expect(model_name.name).to eq('Limber::TubeRack')
+ expect(model_name.singular).to eq('limber_tube_rack')
+ expect(model_name.plural).to eq('limber_tube_racks')
+ expect(model_name.element).to eq('tube_rack')
+ expect(model_name.human).to eq('Tube rack')
+ expect(model_name.collection).to eq('limber/tube_racks')
+ expect(model_name.param_key).to eq('limber_tube_rack')
+ expect(model_name.i18n_key).to eq(:'limber/tube_rack')
+ expect(model_name.route_key).to eq('limber_tube_racks')
+ expect(model_name.singular_route_key).to eq('limber_tube_rack')
+ end
+ end
+end
diff --git a/spec/sequencescape/api/v2/tube_spec.rb b/spec/sequencescape/api/v2/tube_spec.rb
index e7ed47704..fceeaaf7b 100644
--- a/spec/sequencescape/api/v2/tube_spec.rb
+++ b/spec/sequencescape/api/v2/tube_spec.rb
@@ -19,6 +19,7 @@ def where(_arg)
it { is_expected.to_not be_plate }
it { is_expected.to be_tube }
+ it { is_expected.to_not be_tube_rack }
describe '#stock plate' do
let(:stock_plates) { create_list(:v2_stock_plate, 4) }
diff --git a/spec/support/api_url_helper.rb b/spec/support/api_url_helper.rb
index 38bfc39ba..27c9a5519 100644
--- a/spec/support/api_url_helper.rb
+++ b/spec/support/api_url_helper.rb
@@ -76,6 +76,7 @@ def stub_api_put(*components, body:, payload:)
end
end
+ # rubocop:disable Metrics/ModuleLength
module V2Helpers
def stub_api_v2_patch(klass)
# intercepts the 'update' and 'update!' method for any instance of the class beginning with
@@ -176,6 +177,39 @@ def stub_v2_tube(tube, stub_search: true, custom_includes: false)
stub_v2_labware(tube)
end
+ # rubocop:disable Metrics/AbcSize
+ def stub_v2_tube_rack(tube_rack, stub_search: true, custom_query: nil, custom_includes: nil)
+ stub_barcode_search(tube_rack.barcode.machine, tube_rack) if stub_search
+
+ if custom_query
+ allow(Sequencescape::Api::V2).to receive(custom_query.first).with(*custom_query.last).and_return(tube_rack)
+ elsif custom_includes
+ allow(Sequencescape::Api::V2).to receive(:tube_rack_with_custom_includes).with(
+ custom_includes,
+ { uuid: tube_rack.uuid }
+ ).and_return(tube_rack)
+ else
+ allow(Sequencescape::Api::V2).to receive(:tube_rack_for_presenter).with(uuid: tube_rack.uuid).and_return(
+ tube_rack
+ )
+ end
+
+ arguments = [{ uuid: labware.uuid }]
+ allow(Sequencescape::Api::V2::TubeRack).to receive(:find).with(*arguments).and_return([labware])
+ end
+
+ # rubocop:enable Metrics/AbcSize
+
+ def stub_v2_tube_rack_purpose(tube_rack_purpose)
+ arguments = [{ name: tube_rack_purpose[:name] }]
+ allow(Sequencescape::Api::V2::TubeRackPurpose).to receive(:find).with(*arguments).and_return([tube_rack_purpose])
+ end
+
+ def stub_v2_racked_tube(racked_tube)
+ arguments = [{ tube_rack: racked_tube.tube_rack.id, tube: racked_tube.tube.id }]
+ allow(Sequencescape::Api::V2::RackedTube).to receive(:find).with(*arguments).and_return(racked_tube)
+ end
+
def stub_v2_user(user, swipecard = nil)
# Find by UUID
uuid_args = [{ uuid: user.uuid }]
@@ -188,6 +222,8 @@ def stub_v2_user(user, swipecard = nil)
allow(Sequencescape::Api::V2::User).to receive(:find).with(*swipecard_args).and_return([user])
end
end
+
+ # rubocop:enable Metrics/ModuleLength
end
RSpec.configure do |config|