From b581f0d8bbd4e22e2adbfaec70418f35b03c266c Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 2 Oct 2024 10:25:26 +0100 Subject: [PATCH 01/20] fix typo in comment --- app/models/labware_creators/donor_pooling_plate.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b84e321ce917791f716239be76289923f4c5d9e0 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 16 Oct 2024 11:51:49 +0100 Subject: [PATCH 02/20] added purpose config fields and tube rack relationships --- app/sequencescape/sequencescape/api/v2/tube_rack.rb | 9 ++++++--- config/purposes/scrna_core_cell_extraction.yml | 3 +++ spec/factories/purpose_config_factories.rb | 5 ++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/sequencescape/sequencescape/api/v2/tube_rack.rb b/app/sequencescape/sequencescape/api/v2/tube_rack.rb index fd39e5582..64559e505 100644 --- a/app/sequencescape/sequencescape/api/v2/tube_rack.rb +++ b/app/sequencescape/sequencescape/api/v2/tube_rack.rb @@ -5,8 +5,13 @@ class Sequencescape::Api::V2::TubeRack < Sequencescape::Api::V2::Base include WellHelpers::Extensions include Sequencescape::Api::V2::Shared::HasPurpose include Sequencescape::Api::V2::Shared::HasBarcode + include Sequencescape::Api::V2::Shared::HasPolyMetadata has_many :racked_tubes + has_many :ancestors, class_name: 'Sequencescape::Api::V2::Asset' # Having issues with polymorphism, temporary class + has_many :descendants, class_name: 'Sequencescape::Api::V2::Asset' # Having issues with polymorphism, temporary class + has_many :parents, class_name: 'Sequencescape::Api::V2::Asset' # Having issues with polymorphism, temporary class + has_many :children, class_name: 'Sequencescape::Api::V2::Asset' # Having issues with polymorphism, temporary class property :created_at, type: :time property :updated_at, type: :time @@ -15,9 +20,7 @@ class Sequencescape::Api::V2::TubeRack < Sequencescape::Api::V2::Base property :size property :number_or_rows property :number_of_columns - - # Other relationships - # has_one :purpose via Sequencescape::Api::V2::Shared::HasPurpose + property :tuberack_barcode def model_name ::ActiveModel::Name.new(Sequencescape::Api::V2::TubeRack, false, 'Limber::TubeRack') diff --git a/config/purposes/scrna_core_cell_extraction.yml b/config/purposes/scrna_core_cell_extraction.yml index 112f7cc0e..031360150 100644 --- a/config/purposes/scrna_core_cell_extraction.yml +++ b/config/purposes/scrna_core_cell_extraction.yml @@ -96,8 +96,11 @@ LRC Bank Seq: 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 + child_spare_tube_rack_purpose_name: TR LRC Bank Spare + child_tube_rack_metadata_key: 'tube_rack_barcode' :ancestor_stock_tube_purpose_name: LRC Blood Vac :presenter_class: Presenters::SimpleTubePresenter :state_changer_class: StateChangers::AutomaticTubeStateChanger diff --git a/spec/factories/purpose_config_factories.rb b/spec/factories/purpose_config_factories.rb index a5014f1aa..8d95757f8 100644 --- a/spec/factories/purpose_config_factories.rb +++ b/spec/factories/purpose_config_factories.rb @@ -257,8 +257,11 @@ args: { child_seq_tube_purpose_name: 'Seq Child Purpose', child_seq_tube_name_prefix: 'SEQ', + child_seq_tube_rack_purpose_name: 'Seq Child TubeRack Purpose', child_spare_tube_purpose_name: 'Spare Child Purpose', - child_spare_tube_name_prefix: 'SPR' + child_spare_tube_name_prefix: 'SPR', + child_spare_tube_rack_purpose_name: 'Spare Child TubeRack Purpose', + child_tube_rack_metadata_key: 'tube_rack_barcode' } } end From 909255ec6c2f6cbb6529289409ae0fdeee0d4343 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Thu, 17 Oct 2024 12:01:50 +0100 Subject: [PATCH 03/20] Added tube rack purpose v2 resource --- app/sequencescape/sequencescape/api/v2/tube_rack_purpose.rb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/sequencescape/sequencescape/api/v2/tube_rack_purpose.rb diff --git a/app/sequencescape/sequencescape/api/v2/tube_rack_purpose.rb b/app/sequencescape/sequencescape/api/v2/tube_rack_purpose.rb new file mode 100644 index 000000000..496932088 --- /dev/null +++ b/app/sequencescape/sequencescape/api/v2/tube_rack_purpose.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# tube rack purpose resource +class Sequencescape::Api::V2::TubeRackPurpose < Sequencescape::Api::V2::Base +end From 3f858566aeb20aa55666ff0f8a339ba8d96ee8a2 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 25 Oct 2024 11:55:31 +0100 Subject: [PATCH 04/20] checks added to prevent casting inappropriate values --- config/initializers/json_api_casters.rb | 4 ++++ 1 file changed, 4 insertions(+) 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, From ae959c038a3e578a8d7ed0ee3ab239f370341f6a Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 25 Oct 2024 12:17:08 +0100 Subject: [PATCH 05/20] added specific tube rack creation and related api changes --- app/controllers/creation_controller.rb | 2 +- app/frontend/stylesheets/limber/screen.scss | 3 + .../concerns/presenters/creation_behaviour.rb | 4 + app/models/labware_creators.rb | 10 + .../plate_split_to_tube_racks.rb | 424 ++++++++++++------ app/models/limber/multiplexed_library_tube.rb | 4 + app/models/limber/plate.rb | 4 + app/models/limber/tube.rb | 4 + app/models/limber/tube_rack.rb | 20 + app/models/state_changers.rb | 28 ++ app/sequencescape/sequencescape/api/v2.rb | 22 +- .../sequencescape/api/v2/aliquot.rb | 4 +- .../sequencescape/api/v2/asset.rb | 4 + .../sequencescape/api/v2/base.rb | 3 +- .../sequencescape/api/v2/labware.rb | 11 +- .../sequencescape/api/v2/plate.rb | 3 - .../sequencescape/api/v2/request.rb | 4 +- .../sequencescape/api/v2/request_type.rb | 2 + .../api/v2/specific_tube_rack_creation.rb | 8 + .../sequencescape/api/v2/tube_rack.rb | 34 +- app/views/plates/sidebars/_default.html.erb | 1 + .../plate_split_to_tube_racks.html.erb | 0 ..._throughput_scrna_core_cell_extraction.yml | 4 +- .../purposes/scrna_core_cell_extraction.yml | 43 +- config/purposes/tube_racks.yml | 6 - config/routes.rb | 9 +- lib/purpose_config.rb | 81 +++- 27 files changed, 552 insertions(+), 190 deletions(-) create mode 100644 app/models/limber/tube_rack.rb create mode 100644 app/sequencescape/sequencescape/api/v2/specific_tube_rack_creation.rb rename app/views/{tube_creation => tube_rack_creation}/plate_split_to_tube_racks.html.erb (100%) 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 6f538d558..97676d4eb 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; @@ -272,6 +273,7 @@ nav.robots-list { .other-actions { .work-completion-button, .create-plate-button, + .create-tube_rack-button, .create-tube-button { @extend .btn-secondary; } @@ -280,6 +282,7 @@ nav.robots-list { .work-completion-button, .create-plate-button, .create-tube-button, + .create-tube_rack-button, .create-submission-button { @extend .btn-success; } 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/plate_split_to_tube_racks.rb b/app/models/labware_creators/plate_split_to_tube_racks.rb index ca7208100..669756145 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,50 @@ 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", + # :tube_rack_metadata_key=>"tube_rack_barcode", + # :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 +164,13 @@ 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 to the sequencing rack def redirection_target - parent + child_tube_racks[SEQ_TUBE_RACK_NAME] 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,67 @@ 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 + + def tube_rack_metadata_key_from_config + @tube_rack_metadata_key_from_config ||= purpose_config.dig(:creator_class, :args, :child_tube_rack_metadata_key) + end + + def tube_rack_metadata_key + unless tube_rack_metadata_key_from_config + raise "Missing purpose configuration argument 'child_tube_rack_metadata_key'" + end + + tube_rack_metadata_key_from_config + 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 +578,39 @@ 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, + tube_rack_metadata_key: tube_rack_metadata_key, + 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, + tube_rack_metadata_key: tube_rack_metadata_key, + 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 +635,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 +654,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 +693,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. + # + # @return [Hash{String => String}] A hash mapping tube barcodes to their 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]] + # 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..09e72c37e 100644 --- a/app/models/state_changers.rb +++ b/app/models/state_changers.rb @@ -58,6 +58,23 @@ def self.lookup_for(purpose_uuid) details[:state_changer_class].constantize end + # The tube rack state changer is used by TubeRacks. + # It contains tubes. + class TubeRackStateChanger < DefaultStateChanger + # TubeRacks contain tubes + def contents_for(_target_state) + return nil unless FILTER_FAILS_ON.include?(target_state) + + # determine list of tubes requiring the state change + tubes_locations_filtered = labware.tubes.reject { |t| t.state == 'failed' }.map(&:racked_tube.coordinate) + + # if no tubes are in failed state then no need to send the contents subset + return nil if tubes_locations_filtered.length == labware.tubes.count + + tubes_locations_filtered + 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 +146,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..88af02930 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 receptale 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 diff --git a/app/sequencescape/sequencescape/api/v2/aliquot.rb b/app/sequencescape/sequencescape/api/v2/aliquot.rb index 0f3c4960c..5ffde622b 100644 --- a/app/sequencescape/sequencescape/api/v2/aliquot.rb +++ b/app/sequencescape/sequencescape/api/v2/aliquot.rb @@ -1,8 +1,8 @@ # 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 - belongs_to :request, shallow_path: true + # requires shallow path otherwise get a resource not found issue TODO: where/when do we get this? + has_one :request, shallow_path: true has_one :sample has_one :study has_one :project 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 9e13bfd54..3b4c81e72 100644 --- a/app/sequencescape/sequencescape/api/v2/plate.rb +++ b/app/sequencescape/sequencescape/api/v2/plate.rb @@ -21,9 +21,6 @@ class Sequencescape::Api::V2::Plate < Sequencescape::Api::V2::Base has_many :state_changes has_one :custom_metadatum_collection - # 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 64559e505..acc4ac3ad 100644 --- a/app/sequencescape/sequencescape/api/v2/tube_rack.rb +++ b/app/sequencescape/sequencescape/api/v2/tube_rack.rb @@ -1,20 +1,31 @@ # frozen_string_literal: true -# Tube racks can be barcoded, and contain tubes at defined locations. +# 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 Sequencescape::Api::V2::Shared::HasPurpose include Sequencescape::Api::V2::Shared::HasBarcode include Sequencescape::Api::V2::Shared::HasPolyMetadata - has_many :racked_tubes - has_many :ancestors, class_name: 'Sequencescape::Api::V2::Asset' # Having issues with polymorphism, temporary class - has_many :descendants, class_name: 'Sequencescape::Api::V2::Asset' # Having issues with polymorphism, temporary class - has_many :parents, class_name: 'Sequencescape::Api::V2::Asset' # Having issues with polymorphism, temporary class - has_many :children, class_name: 'Sequencescape::Api::V2::Asset' # Having issues with polymorphism, temporary class + 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' # Having issues with polymorphism, temporary class property :name property :size @@ -22,7 +33,10 @@ class Sequencescape::Api::V2::TubeRack < Sequencescape::Api::V2::Base property :number_of_columns property :tuberack_barcode - def model_name - ::ActiveModel::Name.new(Sequencescape::Api::V2::TubeRack, false, 'Limber::TubeRack') + property :created_at, type: :time + property :updated_at, type: :time + + def stock_plate + nil end end diff --git a/app/views/plates/sidebars/_default.html.erb b/app/views/plates/sidebars/_default.html.erb index 23392a0f1..6e01014f4 100644 --- a/app/views/plates/sidebars/_default.html.erb +++ b/app/views/plates/sidebars/_default.html.erb @@ -52,6 +52,7 @@ <% 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 %> 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/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 7399b1cb6..a441fa5b7 100644 --- a/config/purposes/scrna_core_cell_extraction.yml +++ b/config/purposes/scrna_core_cell_extraction.yml @@ -86,11 +86,12 @@ 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 @@ -101,22 +102,36 @@ LRC Bank Seq: child_spare_tube_name_prefix: SPR child_spare_tube_rack_purpose_name: TR LRC Bank Spare child_tube_rack_metadata_key: 'tube_rack_barcode' - :ancestor_stock_tube_purpose_name: LRC Blood Vac - :presenter_class: Presenters::SimpleTubePresenter - :state_changer_class: StateChangers::AutomaticTubeStateChanger + 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/purposes/tube_racks.yml b/config/purposes/tube_racks.yml index 0e9a1e9df..ef1d5f9d8 100644 --- a/config/purposes/tube_racks.yml +++ b/config/purposes/tube_racks.yml @@ -8,9 +8,3 @@ TR Stock 48: Heron Lysed Tube Rack: :asset_type: tube_rack :size: 96 -TR LRC Bank Seq: - :asset_type: tube_rack - :size: 96 -TR LRC Bank Spare: - :asset_type: tube_rack - :size: 96 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', From 2fa7b50a1eb1a25fd5c81a1325de62e793649582 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 25 Oct 2024 14:15:58 +0100 Subject: [PATCH 06/20] added check for blank --- app/helpers/page_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/page_helper.rb b/app/helpers/page_helper.rb index 49f774667..273067575 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) + return if state.blank? tag.span(state.titleize, class: "state-badge #{state}", title: 'Labware State', data: { toggle: 'tooltip' }) end From b39482246829dde29f625e59a66d7f028401034c Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 25 Oct 2024 14:16:44 +0100 Subject: [PATCH 07/20] changed redirect to return to parent plate --- app/models/labware_creators/plate_split_to_tube_racks.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 669756145..3a68936ab 100644 --- a/app/models/labware_creators/plate_split_to_tube_racks.rb +++ b/app/models/labware_creators/plate_split_to_tube_racks.rb @@ -164,9 +164,10 @@ def perform_transfers api.transfer_request_collection.create!(user: user_uuid, transfer_requests: transfer_request_attributes) end - # We will create multiple child tube racks, redirect to the sequencing rack + # We will create multiple child tube racks, redirect back to the parent plate def redirection_target - child_tube_racks[SEQ_TUBE_RACK_NAME] + # NB. if we want to change this to the first tube rack use: child_tube_racks[SEQ_TUBE_RACK_NAME] + parent end # We will want to see the list of tubes in the rack From f64c55be904a620d232b71b61606488df24398c9 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Mon, 28 Oct 2024 16:43:45 +0000 Subject: [PATCH 08/20] added check for tube rack false --- spec/sequencescape/api/v2/plate_spec.rb | 1 + spec/sequencescape/api/v2/tube_spec.rb | 1 + 2 files changed, 2 insertions(+) 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_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) } From eb600c679e7be5c958dcd01405e90f1372454637 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Mon, 28 Oct 2024 16:44:21 +0000 Subject: [PATCH 09/20] added cached relationship for request type to factory --- spec/factories/request_factories.rb | 2 ++ 1 file changed, 2 insertions(+) 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 From e29762a3bcd7f8cfd8c9a0c29ed9ba42ffeb754e Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Mon, 28 Oct 2024 16:49:37 +0000 Subject: [PATCH 10/20] added methods to fetch aliquots and requests_as_source for tubes in the tuberack, used for fetching active requests --- .../sequencescape/api/v2/tube_rack.rb | 34 ++++++- spec/sequencescape/api/v2/tube_rack_spec.rb | 89 +++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 spec/sequencescape/api/v2/tube_rack_spec.rb diff --git a/app/sequencescape/sequencescape/api/v2/tube_rack.rb b/app/sequencescape/sequencescape/api/v2/tube_rack.rb index acc4ac3ad..1c97ed65f 100644 --- a/app/sequencescape/sequencescape/api/v2/tube_rack.rb +++ b/app/sequencescape/sequencescape/api/v2/tube_rack.rb @@ -2,7 +2,7 @@ # 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 Sequencescape::Api::V2::Shared::HasRequests include Sequencescape::Api::V2::Shared::HasPurpose include Sequencescape::Api::V2::Shared::HasBarcode include Sequencescape::Api::V2::Shared::HasPolyMetadata @@ -39,4 +39,36 @@ def model_name 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/spec/sequencescape/api/v2/tube_rack_spec.rb b/spec/sequencescape/api/v2/tube_rack_spec.rb new file mode 100644 index 000000000..1a419ad91 --- /dev/null +++ b/spec/sequencescape/api/v2/tube_rack_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Sequencescape::Api::V2::TubeRack, type: :model do + let!(:tube_rack) { create(:tube_rack, tubes: { A1: tube1, B1: tube2 }) } + + describe '#requests_in_progress' do + 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 + 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 + # allow(tube_rack).to receive(:requests_as_source_for_tubes).and_return([request1]) + # allow(tube_rack).to receive(:requests_in_progress).and_return([request2]) + + expect(tube_rack.all_requests.map(&:id)).to match_array([request1.id, request2.id]) + end + end + end +end From 4e5b29ce7bfcfd8d4f220c3e126359f3f24833cf Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Tue, 29 Oct 2024 09:07:24 +0000 Subject: [PATCH 11/20] removed unneeded comment --- spec/sequencescape/api/v2/tube_rack_spec.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/sequencescape/api/v2/tube_rack_spec.rb b/spec/sequencescape/api/v2/tube_rack_spec.rb index 1a419ad91..a81af1d4f 100644 --- a/spec/sequencescape/api/v2/tube_rack_spec.rb +++ b/spec/sequencescape/api/v2/tube_rack_spec.rb @@ -79,9 +79,6 @@ end it 'returns all requests associated with the tube rack' do - # allow(tube_rack).to receive(:requests_as_source_for_tubes).and_return([request1]) - # allow(tube_rack).to receive(:requests_in_progress).and_return([request2]) - expect(tube_rack.all_requests.map(&:id)).to match_array([request1.id, request2.id]) end end From f2bf2d420fc9092061afe14dd1ebdc177e761057 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Thu, 31 Oct 2024 17:08:29 +0000 Subject: [PATCH 12/20] put WellHelpers include back as breaking tests --- app/sequencescape/sequencescape/api/v2/tube_rack.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/sequencescape/sequencescape/api/v2/tube_rack.rb b/app/sequencescape/sequencescape/api/v2/tube_rack.rb index 1c97ed65f..974575514 100644 --- a/app/sequencescape/sequencescape/api/v2/tube_rack.rb +++ b/app/sequencescape/sequencescape/api/v2/tube_rack.rb @@ -2,6 +2,7 @@ # Tube racks can be barcoded, and contain racked tubes at defined locations. class Sequencescape::Api::V2::TubeRack < Sequencescape::Api::V2::Base + 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 @@ -25,7 +26,7 @@ def model_name end has_many :racked_tubes, class_name: 'Sequencescape::Api::V2::RackedTube' - has_many :parents, class_name: 'Sequencescape::Api::V2::Asset' # Having issues with polymorphism, temporary class + has_many :parents, class_name: 'Sequencescape::Api::V2::Asset' property :name property :size From 57086194396bf28f94055418326b198732b57bdd Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Thu, 31 Oct 2024 17:17:07 +0000 Subject: [PATCH 13/20] fixed tests for labware creator --- spec/factories/purpose_config_factories.rb | 17 +- .../plate_split_to_tube_racks_spec.rb | 472 ++++++++++++++---- spec/support/api_url_helper.rb | 10 + 3 files changed, 401 insertions(+), 98 deletions(-) diff --git a/spec/factories/purpose_config_factories.rb b/spec/factories/purpose_config_factories.rb index d344ef3b1..b2b87f470 100644 --- a/spec/factories/purpose_config_factories.rb +++ b/spec/factories/purpose_config_factories.rb @@ -253,21 +253,26 @@ # 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_seq_tube_rack_purpose_name: 'Seq Child TubeRack Purpose', - child_spare_tube_purpose_name: 'Spare Child Purpose', + 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: 'Spare Child TubeRack Purpose', - child_tube_rack_metadata_key: 'tube_rack_barcode' + child_spare_tube_rack_purpose_name: 'SPR TubeRack Purpose', + child_tube_rack_metadata_key: 'tube_rack_barcode', + 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/models/labware_creators/plate_split_to_tube_racks_spec.rb b/spec/models/labware_creators/plate_split_to_tube_racks_spec.rb index 0e88bd6af..6cf9a93a8 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 @@ -113,9 +127,10 @@ # 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 +145,201 @@ ) 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", + # :tube_rack_metadata_key=>"tube_rack_barcode", + # :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, + tube_rack_metadata_key: 'tube_rack_barcode', + 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 - # Expect the post requests. - expect_api_v2_posts('CustomMetadatumCollection', expected_call_args) + 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, + tube_rack_metadata_key: 'tube_rack_barcode', + 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 + + 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) @@ -250,7 +397,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 +440,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 @@ -370,7 +517,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 +546,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 +578,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 @@ -488,7 +635,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 +671,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 +680,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 +716,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,16 +773,36 @@ 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 @@ -654,7 +821,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 +859,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 +931,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 +998,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 +1079,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 +1125,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 +1176,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 +1199,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/support/api_url_helper.rb b/spec/support/api_url_helper.rb index 38bfc39ba..d090b791f 100644 --- a/spec/support/api_url_helper.rb +++ b/spec/support/api_url_helper.rb @@ -176,6 +176,16 @@ def stub_v2_tube(tube, stub_search: true, custom_includes: false) stub_v2_labware(tube) end + 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 }] From e934d73cf878afda94af192c4fc655f1a87bbf09 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 1 Nov 2024 10:53:07 +0000 Subject: [PATCH 14/20] reverted has_one to belongs_to as breaking tests --- app/sequencescape/sequencescape/api/v2/aliquot.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/sequencescape/sequencescape/api/v2/aliquot.rb b/app/sequencescape/sequencescape/api/v2/aliquot.rb index 5ffde622b..4cc3f87c8 100644 --- a/app/sequencescape/sequencescape/api/v2/aliquot.rb +++ b/app/sequencescape/sequencescape/api/v2/aliquot.rb @@ -2,7 +2,7 @@ class Sequencescape::Api::V2::Aliquot < Sequencescape::Api::V2::Base # rubocop:todo Style/Documentation # requires shallow path otherwise get a resource not found issue TODO: where/when do we get this? - has_one :request, shallow_path: true + belongs_to :request, shallow_path: true has_one :sample has_one :study has_one :project From bdd0c77e9f6703652d273af1a74edc2c12266a0c Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 1 Nov 2024 10:56:52 +0000 Subject: [PATCH 15/20] add WellHelpers back into tube rack as using coordinate methods --- app/sequencescape/sequencescape/api/v2/tube_rack.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/sequencescape/sequencescape/api/v2/tube_rack.rb b/app/sequencescape/sequencescape/api/v2/tube_rack.rb index 974575514..1e28cc6ae 100644 --- a/app/sequencescape/sequencescape/api/v2/tube_rack.rb +++ b/app/sequencescape/sequencescape/api/v2/tube_rack.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +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 # obviously tube racks do not have wells, refactor the helper? @@ -32,7 +34,6 @@ def model_name property :size property :number_or_rows property :number_of_columns - property :tuberack_barcode property :created_at, type: :time property :updated_at, type: :time From f414945d8a94d8e24a3e988e2d830cf0c3daba88 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 1 Nov 2024 15:10:55 +0000 Subject: [PATCH 16/20] added additional unit tests --- .../plate_split_to_tube_racks_spec.rb | 149 ++++++++++++++---- spec/sequencescape/api/v2/tube_rack_spec.rb | 34 +++- 2 files changed, 149 insertions(+), 34 deletions(-) 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 6cf9a93a8..d4623309f 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 @@ -122,6 +122,11 @@ ) 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] } @@ -363,6 +368,114 @@ def expect_specific_tube_rack_creation(child_tube_racks, child_tube_rack_attribu 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 '#tube_rack_metadata_key' do + context 'when child_tube_rack_metadata_key is missing' do + before { allow(subject).to receive(:tube_rack_metadata_key_from_config).and_return(nil) } + + it 'raises an error' do + expect { subject.send(:tube_rack_metadata_key) }.to raise_error( + RuntimeError, + "Missing purpose configuration argument 'child_tube_rack_metadata_key'" + ) + end + 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.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.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 } @@ -370,13 +483,7 @@ def expect_specific_tube_rack_creation(child_tube_racks, child_tube_rack_attribu 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) @@ -493,15 +600,7 @@ def expect_specific_tube_rack_creation(child_tube_racks, child_tube_rack_attribu 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 } @@ -611,15 +710,7 @@ def expect_specific_tube_rack_creation(child_tube_racks, child_tube_rack_attribu 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 } @@ -807,13 +898,7 @@ def expect_specific_tube_rack_creation(child_tube_racks, child_tube_rack_attribu 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 diff --git a/spec/sequencescape/api/v2/tube_rack_spec.rb b/spec/sequencescape/api/v2/tube_rack_spec.rb index a81af1d4f..96b750fa7 100644 --- a/spec/sequencescape/api/v2/tube_rack_spec.rb +++ b/spec/sequencescape/api/v2/tube_rack_spec.rb @@ -3,9 +3,9 @@ require 'rails_helper' RSpec.describe Sequencescape::Api::V2::TubeRack, type: :model do - let!(:tube_rack) { create(:tube_rack, tubes: { A1: tube1, B1: tube2 }) } - 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)]) } @@ -44,6 +44,8 @@ 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)]) } @@ -83,4 +85,32 @@ 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 From 2c74086a2ff328e2388faa8ab7cf6c0436f8cfb6 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 1 Nov 2024 15:20:31 +0000 Subject: [PATCH 17/20] fixed typo --- app/sequencescape/sequencescape/api/v2.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/sequencescape/sequencescape/api/v2.rb b/app/sequencescape/sequencescape/api/v2.rb index 88af02930..9a8985c55 100644 --- a/app/sequencescape/sequencescape/api/v2.rb +++ b/app/sequencescape/sequencescape/api/v2.rb @@ -22,7 +22,7 @@ module Sequencescape::Api::V2 } ].freeze - # NB. a receptale can have many aliquots, and aliquot.request is an array (for some reason) + # 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, From 715c7eaad8d21c12eac9925662aa37a1234be69e Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 1 Nov 2024 15:23:49 +0000 Subject: [PATCH 18/20] fixed test for private method --- .../models/labware_creators/plate_split_to_tube_racks_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 d4623309f..8da71f7a4 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 @@ -435,14 +435,14 @@ def expect_specific_tube_rack_creation(child_tube_racks, child_tube_rack_attribu 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.validate_tube_barcodes_for_well!(tube_barcodes_for_well, well) }.not_to raise_error + 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.validate_tube_barcodes_for_well!(tube_barcodes_for_well, well) }.to raise_error( + 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'" ) From 456d986d223bca451555729613ba28b9dd554a63 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Mon, 4 Nov 2024 15:53:42 +0000 Subject: [PATCH 19/20] added comments and added tests for contents_for in TubeRackStateChanger --- app/models/state_changers.rb | 49 ++++++++++-- app/sequencescape/sequencescape/api/v2.rb | 4 + spec/models/state_changers_spec.rb | 93 ++++++++++++++++++----- spec/support/api_url_helper.rb | 26 +++++++ 4 files changed, 148 insertions(+), 24 deletions(-) diff --git a/app/models/state_changers.rb b/app/models/state_changers.rb index 09e72c37e..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) @@ -59,19 +78,35 @@ def self.lookup_for(purpose_uuid) end # The tube rack state changer is used by TubeRacks. - # It contains tubes. + # It contains racked tubes. class TubeRackStateChanger < DefaultStateChanger - # TubeRacks contain tubes - def contents_for(_target_state) + # 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 - tubes_locations_filtered = labware.tubes.reject { |t| t.state == 'failed' }.map(&:racked_tube.coordinate) + # 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 failed state then no need to send the contents subset - return nil if tubes_locations_filtered.length == labware.tubes.count + # 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 - tubes_locations_filtered + # 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 diff --git a/app/sequencescape/sequencescape/api/v2.rb b/app/sequencescape/sequencescape/api/v2.rb index 9a8985c55..968a079d9 100644 --- a/app/sequencescape/sequencescape/api/v2.rb +++ b/app/sequencescape/sequencescape/api/v2.rb @@ -87,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/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/support/api_url_helper.rb b/spec/support/api_url_helper.rb index d090b791f..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,29 @@ 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]) @@ -198,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| From 4a329b264a4f42ed69db8a1781fc5370f2a86438 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 20 Nov 2024 16:45:12 +0000 Subject: [PATCH 20/20] removed references to tube rack barcode metadata key --- .../plate_split_to_tube_racks.rb | 15 --------------- config/purposes/scrna_core_cell_extraction.yml | 1 - spec/factories/purpose_config_factories.rb | 1 - .../plate_split_to_tube_racks_spec.rb | 16 ---------------- 4 files changed, 33 deletions(-) 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 3a68936ab..296421013 100644 --- a/app/models/labware_creators/plate_split_to_tube_racks.rb +++ b/app/models/labware_creators/plate_split_to_tube_racks.rb @@ -127,7 +127,6 @@ def create_labware! # :tube_rack_name=>"Seq Tube Rack", # :tube_rack_barcode=>"TR00000001", # :tube_rack_purpose_uuid=>"0ab4c9cc-4dad-11ef-8ca3-82c61098d1a1", - # :tube_rack_metadata_key=>"tube_rack_barcode", # :racked_tubes=>[ # { # :tube_barcode=>"SQ45303801", @@ -550,18 +549,6 @@ def contingency_tube_rack_purpose_uuid @contingency_tube_rack_purpose_uuid ||= contingency_tube_rack_purpose.uuid end - def tube_rack_metadata_key_from_config - @tube_rack_metadata_key_from_config ||= purpose_config.dig(:creator_class, :args, :child_tube_rack_metadata_key) - end - - def tube_rack_metadata_key - unless tube_rack_metadata_key_from_config - raise "Missing purpose configuration argument 'child_tube_rack_metadata_key'" - end - - tube_rack_metadata_key_from_config - 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. @@ -584,7 +571,6 @@ 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, - tube_rack_metadata_key: tube_rack_metadata_key, racked_tubes: generate_tube_attributes( 'sequencing', @@ -603,7 +589,6 @@ 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, - tube_rack_metadata_key: tube_rack_metadata_key, racked_tubes: generate_tube_attributes( 'contingency', diff --git a/config/purposes/scrna_core_cell_extraction.yml b/config/purposes/scrna_core_cell_extraction.yml index a441fa5b7..0bcd17d09 100644 --- a/config/purposes/scrna_core_cell_extraction.yml +++ b/config/purposes/scrna_core_cell_extraction.yml @@ -101,7 +101,6 @@ TR LRC Bank Seq: child_spare_tube_purpose_name: LRC Bank Spare child_spare_tube_name_prefix: SPR child_spare_tube_rack_purpose_name: TR LRC Bank Spare - child_tube_rack_metadata_key: 'tube_rack_barcode' ancestor_stock_tube_purpose_name: LRC Blood Vac :presenter_class: Presenters::TubeRackPresenter :state_changer_class: StateChangers::AutomaticTubeRackStateChanger diff --git a/spec/factories/purpose_config_factories.rb b/spec/factories/purpose_config_factories.rb index b2b87f470..32bc28a5f 100644 --- a/spec/factories/purpose_config_factories.rb +++ b/spec/factories/purpose_config_factories.rb @@ -267,7 +267,6 @@ child_spare_tube_purpose_name: 'SPR Tube Purpose', child_spare_tube_name_prefix: 'SPR', child_spare_tube_rack_purpose_name: 'SPR TubeRack Purpose', - child_tube_rack_metadata_key: 'tube_rack_barcode', ancestor_stock_tube_purpose_name: 'Ancestor Tube Purpose' } } 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 8da71f7a4..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 @@ -228,7 +228,6 @@ def prepare_created_child_tubes(tube_attributes, tube_rack) # :tube_rack_name=>"Seq Tube Rack", # :tube_rack_barcode=>"TR00000001", # :tube_rack_purpose_uuid=>"0ab4c9cc-4dad-11ef-8ca3-82c61098d1a1", - # :tube_rack_metadata_key=>"tube_rack_barcode", # :racked_tubes=>[ # { # :tube_barcode=>"SQ45303801", @@ -257,7 +256,6 @@ def generate_child_tube_rack_attributes(params) 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, - tube_rack_metadata_key: 'tube_rack_barcode', racked_tubes: params[:sequencing_tubes].each_with_index.map do |tube, tube_index| { @@ -276,7 +274,6 @@ def generate_child_tube_rack_attributes(params) 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, - tube_rack_metadata_key: 'tube_rack_barcode', racked_tubes: params[:contingency_tubes].each_with_index.map do |tube, tube_index| { @@ -397,19 +394,6 @@ def expect_specific_tube_rack_creation(child_tube_racks, child_tube_rack_attribu end end - describe '#tube_rack_metadata_key' do - context 'when child_tube_rack_metadata_key is missing' do - before { allow(subject).to receive(:tube_rack_metadata_key_from_config).and_return(nil) } - - it 'raises an error' do - expect { subject.send(:tube_rack_metadata_key) }.to raise_error( - RuntimeError, - "Missing purpose configuration argument 'child_tube_rack_metadata_key'" - ) - end - 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') }