diff --git a/app/models/labware_creators/common_file_handling/csv_file/row_base.rb b/app/models/labware_creators/common_file_handling/csv_file/row_base.rb new file mode 100644 index 000000000..5bffb529a --- /dev/null +++ b/app/models/labware_creators/common_file_handling/csv_file/row_base.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Part of the Labware creator classes +module LabwareCreators + module CommonFileHandling + # + # This is an abstract class for handling rows within csv files. + # It provides a simple wrapper for handling and validating an individual row. + # + class CsvFile::RowBase + include ActiveModel::Validations + + def initialize(index, row_data) + @index = index + @row_data = row_data + + initialize_context_specific_fields + end + + # Override in subclass + # Example: + # @my_field_1 = (@row_data[0] || '').strip.upcase + # @my_field_2 = (@row_data[1] || '').strip.upcase + def initialize_context_specific_fields + raise 'Method should be implemented within subclasses' + end + + # Override in subclass + # For use in error messages + # e.g. "row #{index + 2} [#{@my_field_1}]" + def to_s + raise 'Method should be implemented within subclasses' + end + + # Check for whether the row is empty + # Here all? returns true for an empty array, and nil? returns true for nil elements. + # So if @row_data is either empty or all nil, empty? will return true. + def empty? + @row_data.all?(&:nil?) + end + end + end +end diff --git a/app/models/labware_creators/common_file_handling/csv_file/row_for_tube_rack.rb b/app/models/labware_creators/common_file_handling/csv_file/row_for_tube_rack.rb new file mode 100644 index 000000000..176eaf2e6 --- /dev/null +++ b/app/models/labware_creators/common_file_handling/csv_file/row_for_tube_rack.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Part of the Labware creator classes +module LabwareCreators + require_dependency 'labware_creators/common_file_handling/csv_file_for_tube_rack' + + module CommonFileHandling + # + # This is an shared class for handling rows within tube rack csv files. + # It provides a simple wrapper for handling and validating an individual row + # A row in this file should contain a tube location (coordinate within rack) + # and a tube barcode + # i.e. Tube Position, Tube Barcode + # + class CsvFile::RowForTubeRack < CsvFile::RowBase + TUBE_LOCATION_NOT_RECOGNISED = 'contains an invalid coordinate, in %s' + TUBE_BARCODE_MISSING = 'cannot be empty, in %s' + + attr_reader :tube_position, :tube_barcode, :index + + validates :tube_position, + inclusion: { + in: WellHelpers.column_order, + message: ->(object, _data) { TUBE_LOCATION_NOT_RECOGNISED % object } + }, + unless: :empty? + validates :tube_barcode, presence: { message: ->(object, _data) { TUBE_BARCODE_MISSING % object } } + + def initialize_context_specific_fields + @tube_position = (@row_data[0] || '').strip.upcase + @tube_barcode = (@row_data[1] || '').strip.upcase + end + + def to_s + # NB. index is zero based and no header row here + row_number = @index + 1 + @tube_position.present? ? "row #{row_number} [#{@tube_position}]" : "row #{row_number}" + end + end + end +end diff --git a/app/models/labware_creators/common_file_handling/csv_file/row_for_tube_rack_with_rack_barcode.rb b/app/models/labware_creators/common_file_handling/csv_file/row_for_tube_rack_with_rack_barcode.rb new file mode 100644 index 000000000..281f80fec --- /dev/null +++ b/app/models/labware_creators/common_file_handling/csv_file/row_for_tube_rack_with_rack_barcode.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Part of the Labware creator classes +module LabwareCreators + require_dependency 'labware_creators/common_file_handling/csv_file_for_tube_rack_with_rack_barcode' + + module CommonFileHandling + # + # This is a shared class for handling rows within tube rack csv files. + # It provides a simple wrapper for handling and validating an individual row + # A row in this file should contain a tube rack barcode, the tube location (coordinate + # within the rack) and the tube barcode + # i.e. Tube Rack Barcode, Tube Position, Tube Barcode + # + class CsvFile::RowForTubeRackWithRackBarcode < CsvFile::RowForTubeRack + attr_reader :tube_rack_barcode + + TUBE_RACK_BARCODE_MISSING = 'cannot be empty, in %s' + + validates :tube_rack_barcode, presence: { message: ->(object, _data) { TUBE_RACK_BARCODE_MISSING % object } } + + def initialize_context_specific_fields + @tube_rack_barcode = (@row_data[0] || '').strip.upcase + @tube_position = (@row_data[1] || '').strip.upcase + @tube_barcode = (@row_data[2] || '').strip.upcase + end + end + end +end diff --git a/app/models/labware_creators/common_file_handling/csv_file_base.rb b/app/models/labware_creators/common_file_handling/csv_file_base.rb new file mode 100644 index 000000000..ce2abbff9 --- /dev/null +++ b/app/models/labware_creators/common_file_handling/csv_file_base.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require './lib/nested_validation' +require 'csv' + +# Part of the Labware creator classes +module LabwareCreators + # + # This is an abstract class for handling csv files. + # + class CommonFileHandling::CsvFileBase + include ActiveModel::Validations + extend NestedValidation + + validate :must_be_correctly_parsed + + def initialize(file) + initialize_variables(file) + rescue StandardError => e + reset_variables + @parse_error = e.message + ensure + file.rewind + end + + # Override in subclass if needed + def initialize_variables(file) + @filename = file.original_filename + @data = CSV.parse(file.read) + remove_bom + @parsed = true + end + + # Override in subclass if needed + def reset_variables + @filename = nil + @data = [] + @parsed = false + end + + private + + # Checks for file parsed correctly + def must_be_correctly_parsed + return if @parsed + + errors.add(:base, "Could not read csv: #{@parse_error}") + end + + # Removes the byte order marker (BOM) from the first string in the @data array, if present. + # + # @return [void] + def remove_bom + return unless @data.present? && @data[0][0].present? + + # byte order marker will appear at beginning of in first string in @data array + s = @data[0][0] + + # NB. had to make byte order marker string mutable here otherwise get frozen string error + bom = +"\xEF\xBB\xBF" + s_mod = s.gsub!(bom.force_encoding(Encoding::BINARY), '') + + @data[0][0] = s_mod unless s_mod.nil? + end + end +end diff --git a/app/models/labware_creators/common_file_handling/csv_file_for_tube_rack.rb b/app/models/labware_creators/common_file_handling/csv_file_for_tube_rack.rb new file mode 100644 index 000000000..d441636c3 --- /dev/null +++ b/app/models/labware_creators/common_file_handling/csv_file_for_tube_rack.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +# Part of the Labware creator classes +module LabwareCreators + # + # This is an abstract class for handling tube rack csv files. + # + # Takes the user uploaded tube rack scan csv file, validates the content and extracts the information. + # + # This version of the rack scan file contains 2 columns, the first is the tube location (coordinate within rack) + # and the second is the tube barcode. + # + # Example of file content (NB. no header line): + # A1,FX05653780 + # A2,NO READ + # etc. + # + class CommonFileHandling::CsvFileForTubeRack < CommonFileHandling::CsvFileBase + validates_nested :tube_rack_scan + validate :check_no_duplicate_rack_positions + validate :check_no_duplicate_tube_barcodes + + NO_TUBE_TEXTS = ['NO READ', 'NOSCAN', ''].freeze + NO_DUPLICATE_RACK_POSITIONS_MSG = 'contains duplicate rack positions (%s)' + NO_DUPLICATE_TUBE_BARCODES_MSG = 'contains duplicate tube barcodes (%s)' + + # + # Extracts tube details by rack location from the uploaded csv file. + # This hash is useful when we want the details for a rack location. + # + # @return [Hash] e.g. { 'A1' => { details for this location }, 'B1' => etc. } + # + def position_details + @position_details ||= generate_position_details_hash + end + + # This hash is useful when we want to know the location of a tube barcode + # within the rack. + # + # @return [Hash] eg. { 'FX00000001' => 'A1', 'FX00000002 => 'B1' etc. } + # + def location_by_barcode_details + @location_by_barcode_details ||= + position_details.each_with_object({}) { |(position, details), hash| hash[details['tube_barcode']] = position } + end + + private + + # Returns an array of Row objects representing the tube rack scan data in the CSV file. + # + # @return [Array] An array of Row objects. + def tube_rack_scan + @tube_rack_scan ||= + @data[0..].each_with_index.map do |row_data, index| + CommonFileHandling::CsvFile::RowForTubeRack.new(index, row_data) + end + end + + # Checks for duplicate rack positions in the tube rack scan. + # If any duplicates are found, an error message is added to the errors object. + # The error message includes the duplicated rack positions. + # This method is used to ensure that each rack position in the tube rack scan is unique. + def check_no_duplicate_rack_positions + return unless @parsed + + duplicated_rack_positions = + tube_rack_scan.group_by(&:tube_position).select { |_position, tubes| tubes.size > 1 }.keys.join(',') + + return if duplicated_rack_positions.empty? + + errors.add(:base, format(NO_DUPLICATE_RACK_POSITIONS_MSG, duplicated_rack_positions)) + end + + # Checks for duplicate tube barcodes in the tube rack scan. + # If any duplicates are found, they are added to the errors object. + # The error message includes the duplicated tube barcodes. + # 'NO READ' and 'NOSCAN' values are ignored and not considered as duplicates. + # This method is used to ensure that each tube barcode in the tube rack scan is unique. + def check_no_duplicate_tube_barcodes + return unless @parsed + + duplicates = tube_rack_scan.group_by(&:tube_barcode).select { |_tube_barcode, tubes| tubes.size > 1 }.keys + + # remove any NO READ or NOSCAN or empty string values from the duplicates + duplicates = duplicates.reject { |barcode| NO_TUBE_TEXTS.include?(barcode) } + + return if duplicates.empty? + + errors.add(:base, format(NO_DUPLICATE_TUBE_BARCODES_MSG, duplicates.join(','))) + end + + # Generates a hash of position details based on the tube rack scan data in the CSV file. + # + # @return [Hash] A hash of position details, where the keys are positions and the values + # are hashes containing the tube barcode for each position. + def generate_position_details_hash + return {} unless valid? + + tube_rack_scan.each_with_object({}) do |row, position_details_hash| + # ignore blank rows in file + next if row.empty? + + # filter out locations with no tube scanned + next if NO_TUBE_TEXTS.include? row.tube_barcode.strip.upcase + + position = row.tube_position + + # we will use this hash later to create the tubes and store the + # rack barcode in the tube metadata + position_details_hash[position] = format_position_details(row) + end + end + + # Override in subclasses if needed. + # Formats the tube barcode details for a given row. + # This method strips leading and trailing whitespace from the tube barcode and converts it to uppercase. + # @param row [CSV::Row] The row of the CSV file. + # @return [Hash] Hash containing the formatted tube barcode. + def format_position_details(row) + { 'tube_barcode' => format_barcode(row.tube_barcode) } + end + + # Formats the given barcode. + # This method removes leading and trailing whitespace from the barcode and converts it to uppercase. + # @param barcode [String] The barcode to format. + # @return [String] The formatted barcode. + def format_barcode(barcode) + barcode.strip.upcase + end + end +end diff --git a/app/models/labware_creators/common_file_handling/csv_file_for_tube_rack_with_rack_barcode.rb b/app/models/labware_creators/common_file_handling/csv_file_for_tube_rack_with_rack_barcode.rb new file mode 100644 index 000000000..2839f8625 --- /dev/null +++ b/app/models/labware_creators/common_file_handling/csv_file_for_tube_rack_with_rack_barcode.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Part of the Labware creator classes +module LabwareCreators + # + # This is an abstract class for handling tube rack csv files which contain rack barcodes. + # + # Takes the user uploaded tube rack scan csv file, validates the content and extracts the information. + # + # This version of the rack scan file contains 3 columns, the first is the tube rack barcode, the + # second it the tube location (coordinate within rack) and the third is the tube barcode. + # + # Example of file content (NB. no header line): + # TR00012345,A1,FX05653780 + # TR00012345,A2,NO READ + # etc. + # + # + class CommonFileHandling::CsvFileForTubeRackWithRackBarcode < CommonFileHandling::CsvFileForTubeRack + validate :check_for_rack_barcodes_the_same + + RACK_BARCODES_NOT_CONSISTENT_MSG = 'should not contain different rack barcodes (%s)' + + private + + # Returns an array of Row objects representing the tube rack scan data in the CSV file. + # + # @return [Array] An array of Row objects. + def tube_rack_scan + @tube_rack_scan ||= + @data[0..].each_with_index.map do |row_data, index| + CommonFileHandling::CsvFile::RowForTubeRackWithRackBarcode.new(index, row_data) + end + end + + def check_for_rack_barcodes_the_same + return unless @parsed + + tube_rack_barcodes = tube_rack_scan.group_by(&:tube_rack_barcode).keys + + return unless tube_rack_barcodes.size > 1 + + barcodes_str = tube_rack_barcodes.join(',') + errors.add(:base, format(RACK_BARCODES_NOT_CONSISTENT_MSG, barcodes_str)) + end + + def format_position_details(row) + { + 'tube_rack_barcode' => format_barcode(row.tube_rack_barcode), + 'tube_barcode' => format_barcode(row.tube_barcode) + } + end + end +end diff --git a/app/models/labware_creators/custom_pooled_tubes/csv_file.rb b/app/models/labware_creators/custom_pooled_tubes/csv_file.rb index f3486463e..e0a2f6282 100644 --- a/app/models/labware_creators/custom_pooled_tubes/csv_file.rb +++ b/app/models/labware_creators/custom_pooled_tubes/csv_file.rb @@ -8,29 +8,13 @@ module LabwareCreators # rubocop:todo Style/Documentation # Takes the user uploaded csv file and extracts the pool information # Also validates the content of the CSV file. - class CustomPooledTubes::CsvFile - include ActiveModel::Validations - extend NestedValidation - - validate :correctly_parsed? + class CustomPooledTubes::CsvFile < CommonFileHandling::CsvFileBase validates :header_row, presence: true validates_nested :header_row validates_nested :transfers, if: :correctly_formatted? delegate :source_column, :destination_column, :volume_column, to: :header_row - def initialize(file) - @data = CSV.parse(file.read) - remove_bom - @parsed = true - rescue StandardError => e - @data = [] - @parsed = false - @parse_error = e.message - ensure - file.rewind - end - # # Extracts pool information from the uploaded csv file # @@ -43,13 +27,6 @@ def pools @pools ||= generate_pools_hash end - def correctly_parsed? - return true if @parsed - - errors.add(:base, "Could not read csv: #{@parse_error}") - false - end - # Returns the contents of the header row def header_row @header_row ||= Header.new(@data[0]) if @data[0] @@ -57,27 +34,13 @@ def header_row private - # remove byte order marker if present - def remove_bom - return unless @data.present? && @data[0][0].present? - - # byte order marker will appear at beginning of in first string in @data array - s = @data[0][0] - - # NB. had to make byte order marker string mutable here otherwise get frozen string error - bom = +"\xEF\xBB\xBF" - s_mod = s.gsub!(bom.force_encoding(Encoding::BINARY), '') - - @data[0][0] = s_mod unless s_mod.nil? - end - def transfers @transfers ||= @data[1..].each_with_index.map { |row_data, index| Row.new(header_row, index + 2, row_data) } end # Gates looking for wells if the file is invalid def correctly_formatted? - correctly_parsed? && header_row.valid? + @parsed && header_row.valid? end def generate_pools_hash diff --git a/app/models/labware_creators/custom_pooled_tubes/csv_file/row.rb b/app/models/labware_creators/custom_pooled_tubes/csv_file/row.rb index f831bf6ce..cc3f66a82 100644 --- a/app/models/labware_creators/custom_pooled_tubes/csv_file/row.rb +++ b/app/models/labware_creators/custom_pooled_tubes/csv_file/row.rb @@ -7,7 +7,7 @@ module LabwareCreators # rubocop:todo Style/Documentation # Class CsvRow provides a simple wrapper for handling and validating # individual CSV rows # - class CustomPooledTubes::CsvFile::Row + class CustomPooledTubes::CsvFile::Row < CommonFileHandling::CsvFile::RowBase include ActiveModel::Validations MISSING_SOURCE = @@ -47,13 +47,16 @@ class CustomPooledTubes::CsvFile::Row def initialize(header, index, row_data) @header = header - @index = index - @source = (row_data[source_column] || '').strip.upcase - @destination = (row_data[destination_column] || '').strip.downcase + super(index, row_data) + end + + def initialize_context_specific_fields + @source = (@row_data[source_column] || '').strip.upcase + @destination = (@row_data[destination_column] || '').strip.upcase # We use %.to_i to avoid converting nil to 0. This allows us to write less # confusing validation error messages. - @volume = row_data[volume_column]&.to_i + @volume = @row_data[volume_column]&.to_i end def to_s diff --git a/app/models/labware_creators/multi_stamp_tubes_using_tube_rack_scan.rb b/app/models/labware_creators/multi_stamp_tubes_using_tube_rack_scan.rb new file mode 100644 index 000000000..690c8eb60 --- /dev/null +++ b/app/models/labware_creators/multi_stamp_tubes_using_tube_rack_scan.rb @@ -0,0 +1,261 @@ +# frozen_string_literal: true + +require_dependency 'form' +require_dependency 'labware_creators/base' + +module LabwareCreators + # Handles the creation of a plate from a number of tubes using an uploaded tube rack scan file. + # + # The parents are standard tubes. + # + # The user uploads a tube rack scan of the tube barcodes and their positions, and this creator will + # transfer the tubes into wells on the plate. + # + # Inputs: + # 1) A parent tube - the user has clicked the add plate button on a specific tube + # 2) A tube rack scan CSV file - this is a scan of the rack of 2D tube barcodes (the rack is not being tracked) + # + # Outputs: + # 1) A child plate - tubes are stamped into corresponding locations in the plate according to the scan file + # + # Validations - Error message to users if any of these are not met: + # 1) The user must always upload a scan file. Validations of the file must pass and it should parse correctly and + # contain at least one tube barcode. + # 2) The tube barcodes must be unique within the file (exclude NO SCAN, NO READ types). + # 3) The scanned child tube barcode(s) must already exist in the system. List any that do already exist with tube + # barcode and scan file location. + # 4) The labware purpose type of each tube must match the one of the expected ones from a list in the plate purpose + # config. List any that do not match with tube barcode, scan file location and purpose type, and list the expected + # type(s). + # 5) The request on the tube must be active, and must match to one of the expected ones from a list in the plate + # purpose config. This is to check that the tubes are at the appropriate stage of their pipeline to transfer. + # + # rubocop:disable Metrics/ClassLength + class MultiStampTubesUsingTubeRackScan < Base + include LabwareCreators::CustomPage + include SupportParent::TubeOnly + + self.page = 'multi_stamp_tubes_using_tube_rack_scan' + self.attributes += [:file] + + attr_accessor :file + + validates :file, presence: true + + validates_nested :csv_file, if: :file + + # NB. CsvFile checks for duplicated barcodes within the uploaded file (ignores no scan types) + validate :tubes_must_exist_in_lims, if: :file + validate :tubes_must_be_of_expected_purpose_type, if: :file + validate :tubes_must_have_active_requests_of_expected_type, if: :file + + EXPECTED_REQUEST_STATES = %w[pending started].freeze + + def save + # NB. need the && true!! + super && upload_tube_rack_file && true + end + + # Creates child plate, performs transfers. + # + # @return [Boolean] true if the child plate was created successfully. + def create_labware! + plate_creation = + api.pooled_plate_creation.create!(parents: parent_tube_uuids, child_purpose: purpose_uuid, user: user_uuid) + + @child = plate_creation.child + child_v2 = Sequencescape::Api::V2.plate_with_wells(@child.uuid) + + transfer_material_from_parent!(child_v2) + + yield(@child) if block_given? + true + end + + # Returns a CsvFile object for the tube rack scan CSV file, or nil if the file doesn't exist. + def csv_file + @csv_file ||= CommonFileHandling::CsvFileForTubeRack.new(file) if file + end + + def file_valid? + file.present? && csv_file&.valid? + end + + # Fetches all tubes from the CSV file and stores them in a hash indexed by barcode. + # This method uses memoization to avoid fetching the tubes more than once. + # def parent_tubes + # @parent_tubes ||= + # csv_file + # .position_details + # .each_with_object({}) do |(_tube_posn, foreign_barcode), tubes| + # search_params = { barcode: foreign_barcode, includes: Sequencescape::Api::V2::Tube::DEFAULT_INCLUDES } + + # tubes[foreign_barcode] = Sequencescape::Api::V2::Tube.find_by(**search_params) + # end + # end + def parent_tubes + @parent_tubes ||= + csv_file + .position_details + .each_with_object({}) do |(_tube_posn, details_hash), tubes| + foreign_barcode = details_hash['tube_barcode'] + search_params = { barcode: foreign_barcode, includes: Sequencescape::Api::V2::Tube::DEFAULT_INCLUDES } + + tubes[foreign_barcode] = Sequencescape::Api::V2::Tube.find_by(**search_params) + end + end + + # Validates that all parent tubes in the CSV file exist in the LIMS. + # Adds an error message for each tube that doesn't exist. + def tubes_must_exist_in_lims + return unless file_valid? + + parent_tubes.each do |foreign_barcode, tube_in_db| + next if tube_in_db.present? + + msg = + "Tube barcode #{foreign_barcode} not found in the LIMS. " \ + 'Please check the tube barcodes in the scan file are valid tubes.' + errors.add(:base, msg) + end + end + + # Validates that all tubes in the parent_tubes hash are of the expected purpose type. + # If a tube is not of the expected purpose type, an error message is added to the errors object. + # Tubes that are not found in the database or are of the expected purpose type are skipped. + # This method is used to ensure that all tubes are of the correct type before starting the transfer. + def tubes_must_be_of_expected_purpose_type + return unless file_valid? + + parent_tubes.each do |foreign_barcode, tube_in_db| + # NB. should be catching missing tubes in previous validation + next if tube_in_db.blank? || expected_tube_purpose_names.include?(tube_in_db.purpose.name) + + msg = + "Tube barcode #{foreign_barcode} does not match to one of the expected tube purposes (one of type(s): #{ + expected_tube_purpose_names.join(', ') + })" + errors.add(:base, msg) + end + end + + # Validates that all tubes in the parent_tubes hash have at least one active request of the expected type. + # If a tube does not have an active request of the expected type, an error message is added to the errors object. + # Tubes that are not found in the database or already have an expected active request are skipped. + # This method is used to ensure that all tubes are ready for processing before starting the transfer. + def tubes_must_have_active_requests_of_expected_type + return unless file_valid? + + parent_tubes.each do |foreign_barcode, tube_in_db| + # NB. should be catching missing tubes in previous validation + next if tube_in_db.blank? || tube_has_expected_active_request?(tube_in_db) + + msg = + "Tube barcode #{foreign_barcode} does not have an expected active request (one of type(s): #{ + expected_request_type_keys.join(', ') + })" + errors.add(:base, msg) + end + end + + def expected_request_type_keys + purpose_config.dig(:creator_class, :args, :expected_request_type_keys).to_a + end + + def expected_tube_purpose_names + purpose_config.dig(:creator_class, :args, :expected_tube_purpose_names).to_a + end + + def filename_for_tube_rack_scan + purpose_config.dig(:creator_class, :args, :filename_for_tube_rack_scan) + end + + private + + # Returns an array of unique UUIDs for all parent tubes. + # @return [Array] An array of UUIDs. + def parent_tube_uuids + parent_tubes.values.pluck(:uuid).uniq + end + + def child_v1 + @child_v1 ||= api.plate.find(@child.uuid) + end + + # Uploads the tube rack scan CSV file to the child plate using api v1. + def upload_tube_rack_file + child_v1.qc_files.create_from_file!(file, filename_for_tube_rack_scan) + end + + # Returns an array of active requests of the expected type for the given tube. + # @param tube [Sequencescape::Api::V2::Tube] The tube to get the requests for. + # @return [Array] An array of requests. + def active_requests_of_expected_type(tube) + tube.receptacle.requests_as_source.select do |req| + expected_request_type_keys.include?(req.request_type.key) && EXPECTED_REQUEST_STATES.include?(req.state) + end + end + + # Checks if the given tube has any active requests of the expected type. + # @param tube_in_db [Sequencescape::Api::V2::Tube] The tube to check. + # @return [Boolean] True if the tube has any active requests of the expected type, false otherwise. + def tube_has_expected_active_request?(tube_in_db) + active_requests_of_expected_type(tube_in_db).any? + end + + # Transfers material from the parent tubes to the given child plate. + # @param child_plate [Sequencescape::Api::V2::Plate] The plate to transfer material to. + def transfer_material_from_parent!(child_plate) + api.transfer_request_collection.create!( + user: user_uuid, + transfer_requests: transfer_request_attributes(child_plate) + ) + end + + # Returns an array of hashes representing the transfer requests for the given child plate. + # Each hash includes the UUIDs of the parent tube and child well, and the UUID of the outer request. + # @param child_plate [Sequencescape::Api::V2::Plate] The plate to get the transfer requests for. + # @return [Array] An array of hashes representing the transfer requests. + def transfer_request_attributes(child_plate) + parent_tubes.each_with_object([]) do |(foreign_barcode, parent_tube), tube_transfers| + tube_transfers << + request_hash( + parent_tube.uuid, + child_plate + .wells + .detect { |child_well| child_well.location == csv_file.location_by_barcode_details[foreign_barcode] } + &.uuid, + { outer_request: source_tube_outer_request_uuid(parent_tube) } + ) + end + end + + # Generates a transfer request hash for the given source well UUID, target tube UUID, and additional parameters. + # + # @param source_tube_uuid [String] The UUID of the source tube. + # @param target_plate_uuid [String] The UUID of the target plate. + # @param additional_parameters [Hash] Additional parameters to include in the transfer request hash. + # @return [Hash] A transfer request hash. + def request_hash(source_tube_uuid, target_plate_uuid, additional_parameters) + { 'source_asset' => source_tube_uuid, 'target_asset' => target_plate_uuid }.merge(additional_parameters) + end + + # Returns the UUID of the first active request of the expected type for the given tube. + # It assumes that there should be exactly one such request. + # If no such request is found, it raises an error. + # This method is used to get the UUID of the outer request when creating a transfer. + # + # @param tube [Sequencescape::Api::V2::Tube] The tube to get the request UUID for. + # @return [String] The UUID of the first active request of the expected type. + # @raise [RuntimeError] If no active request of the expected type is found for the tube. + def source_tube_outer_request_uuid(tube) + requests = active_requests_of_expected_type(tube) + + # The validation to check for suitable requests should have caught this + raise "No active request of expected type found for tube #{tube.human_barcode}" if requests.empty? + + requests.first.uuid + end + end + # rubocop:enable Metrics/ClassLength +end diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/row_base.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/row_base.rb index ff3872ebe..11ffb33b9 100644 --- a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/row_base.rb +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file/row_base.rb @@ -6,9 +6,7 @@ module LabwareCreators # Provides a simple wrapper for handling and validating individual CSV rows. # Abstract class, extend for uses in specific pipelines. # - class PcrCyclesBinnedPlate::CsvFile::RowBase - include ActiveModel::Validations - + class PcrCyclesBinnedPlate::CsvFile::RowBase < CommonFileHandling::CsvFile::RowBase IN_RANGE = 'is empty or contains a value that is out of range (%s to %s), in %s' WELL_NOT_RECOGNISED = 'contains an invalid well name: %s' @@ -46,13 +44,17 @@ class PcrCyclesBinnedPlate::CsvFile::RowBase :pcr_cycles_column, to: :header - # rubocop:todo Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity def initialize(row_config, header, index, row_data) @row_config = row_config @header = header - @index = index - @row_data = row_data + super(index, row_data) + + # initialize pipeline specific columns + initialize_pipeline_specific_columns + end + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def initialize_context_specific_fields # initialize supplied fields @well = (@row_data[well_column] || '').strip.upcase @concentration = @row_data[concentration_column]&.strip&.to_f @@ -65,17 +67,16 @@ def initialize(row_config, header, index, row_data) @sample_volume = @row_data[sample_volume_column]&.strip&.to_f @diluent_volume = @row_data[diluent_volume_column]&.strip&.to_f @pcr_cycles = @row_data[pcr_cycles_column]&.strip&.to_i - - initialize_pipeline_specific_columns end - # rubocop:enable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def initialize_pipeline_specific_columns raise '#initialize_pipeline_specific_columns must be implemented on subclasses' end def to_s + # Have a header line so add 2 to index @well.present? ? "row #{index + 2} [#{@well}]" : "row #{index + 2}" end diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_base.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_base.rb index 11faeeed3..60fe2ded4 100644 --- a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_base.rb +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_base.rb @@ -14,11 +14,7 @@ module LabwareCreators # in the child dilution plate. # This is the abstract version of this labware creator, extend from this class # - class PcrCyclesBinnedPlate::CsvFileBase - include ActiveModel::Validations - extend NestedValidation - - validate :correctly_parsed? + class PcrCyclesBinnedPlate::CsvFileBase < CommonFileHandling::CsvFileBase validates :plate_barcode_header_row, presence: true validates_nested :plate_barcode_header_row validates :well_details_header_row, presence: true @@ -43,25 +39,15 @@ class PcrCyclesBinnedPlate::CsvFileBase # Passing in the file to be parsed, the configuration that holds validation range thresholds, and # the parent plate barcode for validation that we are processing the correct file. def initialize(file, config, parent_barcode) - initialize_variables(file, config, parent_barcode) - rescue StandardError => e - reset_variables - @parse_error = e.message - ensure - file.rewind - end - - def initialize_variables(file, config, parent_barcode) + super(file) @config = get_config_details_from_purpose(config) @parent_barcode = parent_barcode - @data = CSV.parse(file.read) - remove_bom - @parsed = true end def reset_variables @config = nil @parent_barcode = nil + @filename = nil @data = [] @parsed = false end @@ -76,13 +62,6 @@ def well_details @well_details ||= generate_well_details_hash end - def correctly_parsed? - return true if @parsed - - errors.add(:base, "Could not read csv: #{@parse_error}") - false - end - def plate_barcode_header_row # data[0] here is the first row in the uploaded file, and should contain the plate barcode @plate_barcode_header_row ||= @@ -100,20 +79,6 @@ def get_config_details_from_purpose(_config) raise '#get_config_details_from_purpose must be implemented on subclasses' end - # remove byte order marker if present - def remove_bom - return unless @data.present? && @data[0][0].present? - - # byte order marker will appear at beginning of in first string in @data array - s = @data[0][0] - - # NB. had to make byte order marker string mutable here otherwise get frozen string error - bom = +"\xEF\xBB\xBF" - s_mod = s.gsub!(bom.force_encoding(Encoding::BINARY), '') - - @data[0][0] = s_mod unless s_mod.nil? - end - def transfers # sample row data starts on third row of file, 1st row is plate barcode header row, second blank @transfers ||= @data[3..].each_with_index.map { |row_data, index| create_row(index, row_data) } @@ -125,7 +90,7 @@ def create_row(_index, _row_data) # Gates looking for wells if the file is invalid def correctly_formatted? - correctly_parsed? && plate_barcode_header_row.valid? && well_details_header_row.valid? + @parsed && plate_barcode_header_row.valid? && well_details_header_row.valid? end # Create the hash of well details from the file upload values diff --git a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq.rb b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq.rb index 24121d6e2..c6b95285e 100644 --- a/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq.rb +++ b/app/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq.rb @@ -36,7 +36,7 @@ def create_row(index, row_data) # Gates looking for wells if the file is invalid def correctly_formatted? - correctly_parsed? && plate_barcode_header_row.valid? && well_details_header_row.valid? + @parsed && plate_barcode_header_row.valid? && well_details_header_row.valid? end end 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 d073b4ef7..8dcbd7186 100644 --- a/app/models/labware_creators/plate_split_to_tube_racks.rb +++ b/app/models/labware_creators/plate_split_to_tube_racks.rb @@ -145,7 +145,8 @@ def anchor # @return [CsvFile, nil] A CsvFile object for the sequencing tube rack scan CSV file, or nil if the file # doesn't exist. def sequencing_csv_file - @sequencing_csv_file ||= CsvFile.new(sequencing_file) if sequencing_file + @sequencing_csv_file ||= + CommonFileHandling::CsvFileForTubeRackWithRackBarcode.new(sequencing_file) if sequencing_file end # Returns a CsvFile object for the contingency tube rack scan CSV file, or nil if the file doesn't exist. @@ -153,7 +154,8 @@ def sequencing_csv_file # @return [CsvFile, nil] A CsvFile object for the contingency tube rack scan CSV file, or nil if the file # doesn't exist. def contingency_csv_file - @contingency_csv_file ||= CsvFile.new(contingency_file) if contingency_file + @contingency_csv_file ||= + CommonFileHandling::CsvFileForTubeRackWithRackBarcode.new(contingency_file) if contingency_file end # Returns the number of unique sample UUIDs for the parent wells after applying the current well filter. diff --git a/app/models/labware_creators/plate_split_to_tube_racks/csv_file.rb b/app/models/labware_creators/plate_split_to_tube_racks/csv_file.rb deleted file mode 100644 index 3320b07cf..000000000 --- a/app/models/labware_creators/plate_split_to_tube_racks/csv_file.rb +++ /dev/null @@ -1,158 +0,0 @@ -# frozen_string_literal: true - -require './lib/nested_validation' -require 'csv' - -# Part of the Labware creator classes -module LabwareCreators - require_dependency 'labware_creators/plate_split_to_tube_racks' - - # - # Takes the user uploaded tube rack scan csv file, validates the content and extracts the information. - # This file will be used to determine and create the fluidX tubes into which samples will be transferred, - # and the tube locations then used to create a driver file for the liquid handler. - # The filename of the file should also contain the tube rack barcode. - # Example of file content (NB. no header line): - # TR00012345,A1,FX05653780 - # TR00012345,A2,NO READ - # etc. - # - class PlateSplitToTubeRacks::CsvFile - include ActiveModel::Validations - extend NestedValidation - - validate :correctly_parsed? - validates_nested :tube_rack_scan, if: :correctly_formatted? - validate :check_for_rack_barcodes_the_same, if: :correctly_formatted? - validate :check_no_duplicate_well_coordinates, if: :correctly_formatted? - validate :check_no_duplicate_tube_barcodes, if: :correctly_formatted? - - NO_TUBE_TEXTS = ['NO READ', 'NOSCAN'].freeze - - def initialize(file) - initialize_variables(file) - rescue StandardError => e - reset_variables - @parse_error = e.message - ensure - file.rewind - end - - def initialize_variables(file) - @filename = file.original_filename - @data = CSV.parse(file.read) - remove_bom - @parsed = true - end - - def reset_variables - @parent_barcode = nil - @filename = nil - @data = [] - @parsed = false - end - - # - # Extracts tube details by rack location from the uploaded csv file. - # This hash is useful when we want the details for a rack location. - # - # @return [Hash] eg. { 'A1' => { 'tube_rack_barcode' => 'TR00000001', - # 'tube_barcode' => 'FX00000001' }, 'B1' => etc. } - # - def position_details - @position_details ||= generate_position_details_hash - end - - def correctly_parsed? - return true if @parsed - - errors.add(:base, "Could not read csv: #{@parse_error}") - false - end - - private - - # Removes the byte order marker (BOM) from the first string in the @data array, if present. - # - # @return [void] - def remove_bom - return unless @data.present? && @data[0][0].present? - - # byte order marker will appear at beginning of in first string in @data array - s = @data[0][0] - - # NB. had to make byte order marker string mutable here otherwise get frozen string error - bom = +"\xEF\xBB\xBF" - s_mod = s.gsub!(bom.force_encoding(Encoding::BINARY), '') - - @data[0][0] = s_mod unless s_mod.nil? - end - - # Returns an array of Row objects representing the tube rack scan data in the CSV file. - # - # @return [Array] An array of Row objects. - def tube_rack_scan - @tube_rack_scan ||= @data[0..].each_with_index.map { |row_data, index| Row.new(index, row_data) } - end - - # Gates looking for tube locations if the file is invalid - def correctly_formatted? - correctly_parsed? - end - - def check_for_rack_barcodes_the_same - tube_rack_barcodes = tube_rack_scan.group_by(&:tube_rack_barcode).keys - - return unless tube_rack_barcodes.size > 1 - - barcodes_str = tube_rack_barcodes.join(',') - errors.add(:base, "should not contain different rack barcodes (#{barcodes_str})") - end - - def check_no_duplicate_well_coordinates - duplicated_well_coordinates = - tube_rack_scan.group_by(&:tube_position).select { |_position, tubes| tubes.size > 1 }.keys.join(',') - - return if duplicated_well_coordinates.empty? - - errors.add(:base, "contains duplicate well coordinates (#{duplicated_well_coordinates})") - end - - def check_no_duplicate_tube_barcodes - duplicates = tube_rack_scan.group_by(&:tube_barcode).select { |_tube_barcode, tubes| tubes.size > 1 }.keys - - # remove any NO READ or NOSCAN values - ignore_list = ['NO READ', 'NOSCAN'] - duplicates = duplicates.reject { |barcode| ignore_list.include?(barcode) } - - return if duplicates.empty? - - errors.add(:base, "contains duplicate tube barcodes (#{duplicates.join(',')})") - end - - # Generates a hash of position details based on the tube rack scan data in the CSV file. - # - # @return [Hash] A hash of position details, where the keys are positions and the values - # are hashes containing the tube rack barcode and tube barcode for each position. - def generate_position_details_hash - return {} unless valid? - - tube_rack_scan.each_with_object({}) do |row, position_details_hash| - # ignore blank rows in file - next if row.empty? - - # filter out locations with no tube scanned - next if NO_TUBE_TEXTS.include? row.tube_barcode.strip.upcase - - position = row.tube_position - - # we will use this hash later to create the tubes and store the - # rack barcode in the tube metadata - position_details_hash[position] = { - 'tube_rack_barcode' => row.tube_rack_barcode.strip.upcase, - 'tube_barcode' => row.tube_barcode.strip.upcase - } - end - end - end -end diff --git a/app/models/labware_creators/plate_split_to_tube_racks/csv_file/row.rb b/app/models/labware_creators/plate_split_to_tube_racks/csv_file/row.rb deleted file mode 100644 index 7b12bd0f7..000000000 --- a/app/models/labware_creators/plate_split_to_tube_racks/csv_file/row.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -# Part of the Labware creator classes -module LabwareCreators - require_dependency 'labware_creators/plate_split_to_tube_racks/csv_file' - - # - # Provides a simple wrapper for handling and validating an individual row - # A row in this file should contain a tube rack barcode (orientation barcode), - # tube location (coordinate within rack) and a tube barcode - # i.e. Tube Rack Barcode, Tube Position, Tube Barcode - # - class PlateSplitToTubeRacks::CsvFile::Row - include ActiveModel::Validations - - TUBE_LOCATION_NOT_RECOGNISED = 'contains an invalid coordinate, in %s' - TUBE_BARCODE_MISSING = 'cannot be empty, in %s' - TUBE_RACK_BARCODE_MISSING = 'cannot be empty, in %s' - - attr_reader :tube_rack_barcode, :tube_position, :tube_barcode, :index - - validates :tube_rack_barcode, presence: { message: ->(object, _data) { TUBE_RACK_BARCODE_MISSING % object } } - validates :tube_position, - inclusion: { - in: WellHelpers.column_order, - message: ->(object, _data) { TUBE_LOCATION_NOT_RECOGNISED % object } - }, - unless: :empty? - validates :tube_barcode, presence: { message: ->(object, _data) { TUBE_BARCODE_MISSING % object } } - - def initialize(index, row_data) - @index = index - @row_data = row_data - - # initialize supplied fields - @tube_rack_barcode = (@row_data[0] || '').strip.upcase - @tube_position = (@row_data[1] || '').strip.upcase - @tube_barcode = (@row_data[2] || '').strip.upcase - end - - def to_s - @tube_position.present? ? "row #{index + 2} [#{@tube_position}]" : "row #{index + 2}" - end - - def empty? - @row_data.empty? || @row_data.compact.empty? - end - end -end diff --git a/app/models/labware_creators/pooled_tubes_by_sample.rb b/app/models/labware_creators/pooled_tubes_by_sample.rb index b4da80aa4..29121318a 100644 --- a/app/models/labware_creators/pooled_tubes_by_sample.rb +++ b/app/models/labware_creators/pooled_tubes_by_sample.rb @@ -118,7 +118,7 @@ def tube_attributes pool_details[:destination_tube_posn] = tube_posn name_for_details = { source_tube_barcode: pool_details[:source_tube_barcode], destination_tube_posn: tube_posn } - { name: name_for(name_for_details), foreign_barcode: csv_file.position_details[tube_posn]['barcode'] } + { name: name_for(name_for_details), foreign_barcode: csv_file.position_details[tube_posn]['tube_barcode'] } end end @@ -140,11 +140,9 @@ def upload_file # # Create class that will parse and validate the uploaded file - # TODO: is there any point passing in the parent barcode if we can't validate the tube rack barcode? - # If they could get the rack barcode into either the csv filename or - # embedded as a field in the file we could validate it. + # def csv_file - @csv_file ||= CsvFile.new(file, parent.human_barcode) + @csv_file ||= CommonFileHandling::CsvFileForTubeRack.new(file) if file end # diff --git a/app/models/labware_creators/pooled_tubes_by_sample/csv_file.rb b/app/models/labware_creators/pooled_tubes_by_sample/csv_file.rb deleted file mode 100644 index 1ff013242..000000000 --- a/app/models/labware_creators/pooled_tubes_by_sample/csv_file.rb +++ /dev/null @@ -1,111 +0,0 @@ -# frozen_string_literal: true - -require './lib/nested_validation' -require 'csv' - -# Part of the Labware creator classes -module LabwareCreators - require_dependency 'labware_creators/pooled_tubes_by_sample' - - # - # Takes the user uploaded tube rack scan csv file, validates the content and extracts the information. - # This file will be used to determine and create the fluidX tubes into which samples will be transferred, - # and the tube locations then used to create a driver file for the liquid handler. - # Example of content (NB. no header): - # A1, FR05653780 - # A2, NO READ - # etc. - # - class PooledTubesBySample::CsvFile - include ActiveModel::Validations - extend NestedValidation - - validate :correctly_parsed? - validates_nested :tube_rack_scan, if: :correctly_formatted? - - NO_TUBE_TEXTS = ['NO READ', 'NOSCAN'].freeze - - # - # Passing in the file to be parsed, the configuration from the purposes yml, and - # the parent plate barcode for validation that we are processing the correct file. - def initialize(file, parent_barcode) - initialize_variables(file, parent_barcode) - rescue StandardError => e - reset_variables - @parse_error = e.message - ensure - file.rewind - end - - def initialize_variables(file, parent_barcode) - @parent_barcode = parent_barcode - @data = CSV.parse(file.read) - remove_bom - @parsed = true - end - - def reset_variables - @parent_barcode = nil - @data = [] - @parsed = false - end - - # - # Extracts tube location details from the uploaded csv file - # - # @return [Hash] eg. { 'A1' => { 'barcode' => AB12345678 }, 'B1' => etc. } - # - def position_details - @position_details ||= generate_position_details_hash - end - - def correctly_parsed? - return true if @parsed - - errors.add(:base, "Could not read csv: #{@parse_error}") - false - end - - private - - # remove byte order marker if present - def remove_bom - return unless @data.present? && @data[0][0].present? - - # byte order marker will appear at beginning of in first string in @data array - s = @data[0][0] - - # NB. had to make byte order marker string mutable here otherwise get frozen string error - bom = +"\xEF\xBB\xBF" - s_mod = s.gsub!(bom.force_encoding(Encoding::BINARY), '') - - @data[0][0] = s_mod unless s_mod.nil? - end - - def tube_rack_scan - @tube_rack_scan ||= @data[0..].each_with_index.map { |row_data, index| Row.new(index, row_data) } - end - - # Gates looking for tube locations if the file is invalid - def correctly_formatted? - correctly_parsed? - end - - # Create the hash of tube location details from the file upload values - # TODO does this need to be sorted in position order? - def generate_position_details_hash - return {} unless valid? - - tube_rack_scan.each_with_object({}) do |row, position_details_hash| - # ignore blank rows in file - next if row.empty? - - # filter out locations with no tube scanned - next if NO_TUBE_TEXTS.include? row.barcode.strip.upcase - - position = row.position - position_details_hash[position] = { 'barcode' => row.barcode.strip.upcase } - end - end - end -end diff --git a/app/models/labware_creators/pooled_tubes_by_sample/csv_file/row.rb b/app/models/labware_creators/pooled_tubes_by_sample/csv_file/row.rb deleted file mode 100644 index f1b059075..000000000 --- a/app/models/labware_creators/pooled_tubes_by_sample/csv_file/row.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -# Part of the Labware creator classes -module LabwareCreators - require_dependency 'labware_creators/pooled_tubes_by_sample/csv_file' - - # - # Provides a simple wrapper for handling and validating an individual row - # A row in this file should contain a tube location (coordinate within rack) and a tube barcode e.g. Location, Barcode - # - class PooledTubesBySample::CsvFile::Row - include ActiveModel::Validations - - TUBE_LOCATION_NOT_RECOGNISED = 'contains an invalid coordinate, in %s' - BARCODE_MISSING = 'cannot be empty, in %s' - - attr_reader :position, :barcode, :index - - validates :position, - inclusion: { - in: WellHelpers.column_order, - message: ->(object, _data) { TUBE_LOCATION_NOT_RECOGNISED % object } - }, - unless: :empty? - validates :barcode, presence: { message: ->(object, _data) { BARCODE_MISSING % object } } - - def initialize(index, row_data) - @index = index - @row_data = row_data - - # initialize supplied fields - @position = (@row_data[0] || '').strip.upcase - @barcode = (@row_data[1] || '').strip.upcase - end - - def to_s - @position.present? ? "row #{index + 2} [#{@position}]" : "row #{index + 2}" - end - - def empty? - @row_data.empty? || @row_data.compact.empty? - end - end -end diff --git a/app/models/state_changers.rb b/app/models/state_changers.rb index c70099ef7..6ab023ae2 100644 --- a/app/models/state_changers.rb +++ b/app/models/state_changers.rb @@ -69,9 +69,10 @@ def contents_for(_target_state) end # Plate state changer to automatically complete specified work requests. - class AutomaticPlateStateChanger < DefaultStateChanger + # This is the abstract version. + class AutomaticLabwareStateChanger < DefaultStateChanger def v2_labware - @v2_labware ||= Sequencescape::Api::V2.plate_for_completion(labware_uuid) + raise 'Must be implemented on subclass' end def purpose_uuid @@ -95,10 +96,29 @@ def move_to!(state, reason = nil, customer_accepts_responsibility = false) # rubocop:enable Style/OptionalBooleanParameter def complete_outstanding_requests - in_prog_submissions = v2_labware.in_progress_submission_uuids(request_type_key: work_completion_request_type) + in_prog_submissions = + v2_labware.in_progress_submission_uuids(request_type_to_complete: work_completion_request_type) return if in_prog_submissions.blank? api.work_completion.create!(submissions: in_prog_submissions, target: v2_labware.uuid, user: user_uuid) end end + + # This version of the AutomaticLabwareStateChanger is used by Plates. + class AutomaticPlateStateChanger < AutomaticLabwareStateChanger + def v2_labware + @v2_labware ||= Sequencescape::Api::V2.plate_for_completion(labware_uuid) + end + end + + # This version of the AutomaticLabwareStateChanger is used by Tubes. + class AutomaticTubeStateChanger < AutomaticLabwareStateChanger + def v2_labware + @v2_labware ||= Sequencescape::Api::V2.tube_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 637fd3bce..1b5af806e 100644 --- a/app/sequencescape/sequencescape/api/v2.rb +++ b/app/sequencescape/sequencescape/api/v2.rb @@ -49,13 +49,26 @@ def self.plate_with_wells(uuid) end def self.tube_rack_for_presenter(query) - TubeRack.includes('racked_tubes.tube.purpose,racked_tubes.tube.aliquots.request.request_type').find(query).first + TubeRack + .includes( + 'racked_tubes.tube.purpose,' \ + 'racked_tubes.tube.receptacle.aliquots.request.request_type' + ) + .find(query) + .first end def self.plate_for_completion(uuid) Plate.includes('wells.aliquots.request.submission,wells.aliquots.request.request_type').find(uuid: uuid).first end + def self.tube_for_completion(uuid) + Tube + .includes('receptacle.aliquots.request.submission,receptacle.aliquots.request.request_type') + .find(uuid: 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/receptacle.rb b/app/sequencescape/sequencescape/api/v2/receptacle.rb index 44da89cfd..b940df168 100644 --- a/app/sequencescape/sequencescape/api/v2/receptacle.rb +++ b/app/sequencescape/sequencescape/api/v2/receptacle.rb @@ -4,6 +4,7 @@ class Sequencescape::Api::V2::Receptacle < Sequencescape::Api::V2::Base has_many :requests_as_source, class_name: 'Sequencescape::Api::V2::Request' has_many :qc_results, class_name: 'Sequencescape::Api::V2::QcResult' + has_many :aliquots, class_name: 'Sequencescape::Api::V2::Aliquot' def latest_molarity latest_qc(key: 'molarity', units: 'nM') diff --git a/app/sequencescape/sequencescape/api/v2/shared/has_requests.rb b/app/sequencescape/sequencescape/api/v2/shared/has_requests.rb index 797c8d0bc..1925b7a08 100644 --- a/app/sequencescape/sequencescape/api/v2/shared/has_requests.rb +++ b/app/sequencescape/sequencescape/api/v2/shared/has_requests.rb @@ -56,19 +56,21 @@ def pool_id submission_ids.first end - # Finding 'in_progress' requests + # Finding in progress requests (set directly on aliquots on transfer into a new labware) + def requests_in_progress(request_type_to_complete: nil) + requests = aliquots&.flat_map(&:request)&.compact + return [] if requests.blank? - def requests_in_progress(request_type_key: nil) - aliquots - .flat_map(&:request) - .compact - .select { |r| request_type_key.nil? || r.request_type_key == request_type_key } + if request_type_to_complete.present? + requests.select { |r| r.request_type_key == request_type_to_complete } + else + requests + end end # Based on in_progress requests - - def in_progress_submission_uuids(request_type_key: nil) - requests_in_progress(request_type_key: request_type_key).flat_map(&:submission_uuid).uniq + def in_progress_submission_uuids(request_type_to_complete: nil) + requests_in_progress(request_type_to_complete: request_type_to_complete).flat_map(&:submission_uuid).uniq end def all_requests diff --git a/app/sequencescape/sequencescape/api/v2/tube.rb b/app/sequencescape/sequencescape/api/v2/tube.rb index 3125c1fc6..a5f0dfb57 100644 --- a/app/sequencescape/sequencescape/api/v2/tube.rb +++ b/app/sequencescape/sequencescape/api/v2/tube.rb @@ -7,7 +7,11 @@ class Sequencescape::Api::V2::Tube < Sequencescape::Api::V2::Base include Sequencescape::Api::V2::Shared::HasBarcode include Sequencescape::Api::V2::Shared::HasWorklineIdentifier - DEFAULT_INCLUDES = [:purpose, 'aliquots.request.request_type'].freeze + DEFAULT_INCLUDES = [ + :purpose, + 'receptacle.aliquots.request.request_type', + 'receptacle.requests_as_source.request_type' + ].freeze self.tube = true @@ -19,7 +23,6 @@ class Sequencescape::Api::V2::Tube < Sequencescape::Api::V2::Base has_many :child_tubes, class_name: 'Sequencescape::Api::V2::Tube' has_one :receptacle, class_name: 'Sequencescape::Api::V2::Receptacle' - has_many :aliquots has_many :direct_submissions has_many :state_changes has_many :transfer_requests_as_target, class_name: 'Sequencescape::Api::V2::TransferRequest' @@ -27,6 +30,10 @@ class Sequencescape::Api::V2::Tube < Sequencescape::Api::V2::Base property :created_at, type: :time property :updated_at, type: :time + def aliquots + receptacle&.aliquots + end + def self.find_by(params) options = params.dup includes = options.delete(:includes) || DEFAULT_INCLUDES @@ -40,12 +47,7 @@ def self.find_all(params) Sequencescape::Api::V2::Tube.includes(*includes).where(**options).paginate(paginate).all end - # Dummied out for the moment. But no real reason not to add it to the API. - # This is accessed through the Receptacle - # TODO: allow this method and delegate it? - def requests_as_source - [] - end + delegate :requests_as_source, to: :receptacle # # Override the model used in form/URL helpers diff --git a/app/views/plate_creation/multi_stamp_tubes_using_tube_rack_scan.html.erb b/app/views/plate_creation/multi_stamp_tubes_using_tube_rack_scan.html.erb new file mode 100644 index 000000000..a9b7f4d0e --- /dev/null +++ b/app/views/plate_creation/multi_stamp_tubes_using_tube_rack_scan.html.erb @@ -0,0 +1,37 @@ +<%= page(:'multi-stamp-tubes-using-tube-rack-scan-page') do -%> + <%= content do %> + <%= card title: 'Help' do %> +

Upload the tube rack scan csv file describing the positions and barcodes of the tubes. An example is shown below:

+ + + + + + + + + + + + + + + + + +
A1AB00000001
B1AB00000002
C1AB00000003
D1AB00000004
E1AB00000005
F1AB00000006
G1AB00000007
H1AB00000008
A2AB00000009
B2AB00000010
C2AB00000011
D2NO READ
E2AB00000013
etc...
+

NB. NO READ or NOSCAN are valid values for the second column. This is interpreted as meaning that the tube rack scanner was not able to read a tube barcode in that position. Usually this happens when the position is empty. This position will be empty in the corresponding child plate wells.

+ <% end %> + <% end %> + <%= sidebar do %> + <%= card title: 'File upload' do %> + <%= form_for(@labware_creator, as: :tube, url: limber_plate_tubes_path(@labware_creator.parent)) do |f| %> + <%= f.hidden_field :purpose_uuid %> +
+ <%= f.file_field :file, accept: '.csv', required: true %> +
+ <%= f.submit class: 'btn btn-success' %> + <% end %> + <% end %> + <% end %> +<%- end -%> diff --git a/config/exports/exports.yml b/config/exports/exports.yml index aef1de95d..149fe4c58 100644 --- a/config/exports/exports.yml +++ b/config/exports/exports.yml @@ -125,7 +125,7 @@ hamilton_lca_pbmc_bank_to_lca_bank_stock: ancestor_tube_purpose: LCA Blood Vac bioscan_mbrave: csv: 'exports/bioscan_mbrave' - tube_includes: transfer_requests_as_target.source_asset,aliquots,aliquots.tag.tag_group,aliquots.tag2.tag_group,aliquots.sample.sample_metadata + tube_includes: transfer_requests_as_target.source_asset,receptacle.aliquots,receptacle.aliquots.tag.tag_group,receptacle.aliquots.tag2.tag_group,receptacle.aliquots.sample.sample_metadata tube_selects: aliquot: - tag_index diff --git a/config/pipelines/high_throughput_scrna_core_cdna_prep.wip.yml b/config/pipelines/high_throughput_scrna_core_cdna_prep.wip.yml index 92eed75e1..aa2ec0b70 100644 --- a/config/pipelines/high_throughput_scrna_core_cdna_prep.wip.yml +++ b/config/pipelines/high_throughput_scrna_core_cdna_prep.wip.yml @@ -1,3 +1,4 @@ +--- # Thawing PBMCs and pooling samples from different donors together scRNA Core Donor Pooling: pipeline_group: scRNA Core cDNA Prep @@ -8,7 +9,6 @@ scRNA Core Donor Pooling: LRC Bank Spare: LRC PBMC Cryostor LRC PBMC Cryostor: LRC PBMC Defrost PBS LRC PBMC Defrost PBS: LRC PBMC Pools - # GEM generation and cDNA Prep scRNA Core cDNA Prep: pipeline_group: scRNA Core cDNA Prep diff --git a/config/purposes/scrna_core_cdna_prep.wip.yml b/config/purposes/scrna_core_cdna_prep.wip.yml index d9cdc4309..208a4bb76 100644 --- a/config/purposes/scrna_core_cdna_prep.wip.yml +++ b/config/purposes/scrna_core_cdna_prep.wip.yml @@ -9,8 +9,18 @@ # Cryostor is the name of the buffer used to freeze the PBMCs. LRC PBMC Cryostor: :asset_type: plate + :size: 96 :stock_plate: false :input_plate: false + :creator_class: + name: LabwareCreators::MultiStampTubesUsingTubeRackScan + args: + expected_request_type_keys: + - 'limber_scrna_core_donor_pooling' + expected_tube_purpose_names: + - 'LRC Bank Seq' + - 'LRC Bank Spare' + filename_for_tube_rack_scan: 'scrna_cryostor_tube_rack_scan.csv' # Plate containing defrosted PBMCs in PBS buffer. LRC PBMC Defrost PBS: :asset_type: plate diff --git a/config/purposes/scrna_core_cell_extraction.yml b/config/purposes/scrna_core_cell_extraction.yml index 9263b2d14..44fbb7547 100644 --- a/config/purposes/scrna_core_cell_extraction.yml +++ b/config/purposes/scrna_core_cell_extraction.yml @@ -109,6 +109,8 @@ LRC Bank Seq: child_spare_tube_name_prefix: SPR :ancestor_stock_tube_purpose_name: LRC Blood Vac :presenter_class: Presenters::SimpleTubePresenter + :state_changer_class: StateChangers::AutomaticTubeStateChanger + :work_completion_request_type: 'limber_scrna_core_cell_extraction' # FluidX tube for freezing and output of cell banking protocol LRC Bank Spare: :asset_type: tube @@ -119,3 +121,5 @@ LRC Bank Spare: args: *fluidx_tube_creation_config :ancestor_stock_tube_purpose_name: LRC Blood Vac :presenter_class: Presenters::SimpleTubePresenter + :state_changer_class: StateChangers::AutomaticTubeStateChanger + :work_completion_request_type: 'limber_scrna_core_cell_extraction' diff --git a/docs/creators.md b/docs/creators.md index 1f0de5af1..4b9eff7be 100644 --- a/docs/creators.md +++ b/docs/creators.md @@ -26,8 +26,8 @@ Labware creators are responsible for creating new labware from a parent labware. {include:LabwareCreators::StampedPlate} - Used directly in 112 purposes: - CLCM DNA End Prep, CLCM DNA Lib PCR XP, CLCM RNA End Prep, CLCM RNA Lib PCR XP, CLCM RT PreAmp, GBS Stock, GBS-96 Stock, GnT MDA Norm, GnT Pico End Prep, GnT Pico-XP, GnT scDNA, GnT Stock, LB Cap Lib, LB Cap Lib PCR, LB Cap Lib PCR-XP, LB cDNA, LB cDNA XP, LB Cherrypick, LB End Prep, LB Lib PCR-XP, LB Post Shear, LB Shear, LBB Cherrypick, LBB Lib-XP, LBB Ligation, LBC 3pV3 GEX Frag 2XP, LBC 3pV3 GEX PCR 2XP, LBC 5p GEX Frag 2XP, LBC 5p GEX PCR 2XP, LBC BCR Enrich1 2XSPRI, LBC BCR Enrich2 2XSPRI, LBC BCR Post PCR, LBC Stock, LBC TCR Enrich1 2XSPRI, LBC TCR Enrich2 2XSPRI, LBC TCR Post PCR, LBR Cherrypick, LBR Frag, LBR Frag cDNA, LBR Globin, LBR Globin DNase, LBR mRNA Cap, LBR Ribo DNase, LBR RiboGlobin DNase, LCA 10X cDNA, LCA PBMC, LCA PBMC Bank, LCMB Cherrypick, LCMB End Prep, LCMB Lib PCR-XP, LDS AL Lib, LDS Cherrypick, LDS Lib PCR XP, LDS Stock, LDS Stock XP, LHR End Prep, LHR PCR 1, LHR PCR 2, LHR RT, LHR-384 AL Lib, LHR-384 End Prep, LHR-384 PCR 1, LHR-384 PCR 2, LHR-384 RT, LHR-384 XP, LRC HT 5p cDNA PCR, LRC HT 5p cDNA PCR XP, LRC HT 5p Chip, LRC HT 5p GEMs, LRC PBMC Cryostor, LRC PBMC Defrost PBS, LRC PBMC Pools, LRC PBMC Pools Input, LSW-96 Stock, LTHR PCR 1, LTHR PCR 2, LTHR RT-S, LTHR-384 PCR 1, LTHR-384 PCR 2, LTN AL Lib, LTN Cherrypick, LTN Lib PCR XP, LTN Post Shear, LTN Shear, LTN Stock, LTN Stock XP, PF Cherrypicked, PF End Prep, PF Lib XP, PF Lib XP2, PF Post Shear, PF Post Shear XP, PF Shear, PF-384 End Prep, PF-384 Lib XP2, pWGS-384 AL Lib, pWGS-384 End Prep, RVI Cap Lib, RVI Cap Lib PCR, RVI Cap Lib PCR XP, RVI cDNA XP, RVI Cherrypick, RVI Lib PCR XP, RVI Lig Bind, RVI RT, scRNA cDNA-XP, scRNA End Prep, scRNA Stock, scRNA-384 cDNA-XP, scRNA-384 End Prep, scRNA-384 Stock, and Tag Plate - 384 + Used directly in 111 purposes: + CLCM DNA End Prep, CLCM DNA Lib PCR XP, CLCM RNA End Prep, CLCM RNA Lib PCR XP, CLCM RT PreAmp, GBS Stock, GBS-96 Stock, GnT MDA Norm, GnT Pico End Prep, GnT Pico-XP, GnT scDNA, GnT Stock, LB Cap Lib, LB Cap Lib PCR, LB Cap Lib PCR-XP, LB cDNA, LB cDNA XP, LB Cherrypick, LB End Prep, LB Lib PCR-XP, LB Post Shear, LB Shear, LBB Cherrypick, LBB Lib-XP, LBB Ligation, LBC 3pV3 GEX Frag 2XP, LBC 3pV3 GEX PCR 2XP, LBC 5p GEX Frag 2XP, LBC 5p GEX PCR 2XP, LBC BCR Enrich1 2XSPRI, LBC BCR Enrich2 2XSPRI, LBC BCR Post PCR, LBC Stock, LBC TCR Enrich1 2XSPRI, LBC TCR Enrich2 2XSPRI, LBC TCR Post PCR, LBR Cherrypick, LBR Frag, LBR Frag cDNA, LBR Globin, LBR Globin DNase, LBR mRNA Cap, LBR Ribo DNase, LBR RiboGlobin DNase, LCA 10X cDNA, LCA PBMC, LCA PBMC Bank, LCMB Cherrypick, LCMB End Prep, LCMB Lib PCR-XP, LDS AL Lib, LDS Cherrypick, LDS Lib PCR XP, LDS Stock, LDS Stock XP, LHR End Prep, LHR PCR 1, LHR PCR 2, LHR RT, LHR-384 AL Lib, LHR-384 End Prep, LHR-384 PCR 1, LHR-384 PCR 2, LHR-384 RT, LHR-384 XP, LRC HT 5p cDNA PCR, LRC HT 5p cDNA PCR XP, LRC HT 5p Chip, LRC HT 5p GEMs, LRC PBMC Defrost PBS, LRC PBMC Pools, LRC PBMC Pools Input, LSW-96 Stock, LTHR PCR 1, LTHR PCR 2, LTHR RT-S, LTHR-384 PCR 1, LTHR-384 PCR 2, LTN AL Lib, LTN Cherrypick, LTN Lib PCR XP, LTN Post Shear, LTN Shear, LTN Stock, LTN Stock XP, PF Cherrypicked, PF End Prep, PF Lib XP, PF Lib XP2, PF Post Shear, PF Post Shear XP, PF Shear, PF-384 End Prep, PF-384 Lib XP2, pWGS-384 AL Lib, pWGS-384 End Prep, RVI Cap Lib, RVI Cap Lib PCR, RVI Cap Lib PCR XP, RVI cDNA XP, RVI Cherrypick, RVI Lib PCR XP, RVI Lig Bind, RVI RT, scRNA cDNA-XP, scRNA End Prep, scRNA Stock, scRNA-384 cDNA-XP, scRNA-384 End Prep, scRNA-384 Stock, and Tag Plate - 384 {LabwareCreators::StampedPlate View class documentation} @@ -120,6 +120,16 @@ Labware creators are responsible for creating new labware from a parent labware. {LabwareCreators::MultiStampTubes View class documentation} +## LabwareCreators::MultiStampTubesUsingTubeRackScan + +{include:LabwareCreators::MultiStampTubesUsingTubeRackScan} + + Used directly in 1 purposes: + LRC PBMC Cryostor + +{LabwareCreators::MultiStampTubesUsingTubeRackScan View class documentation} + + ## LabwareCreators::PlateSplitToTubeRacks {include:LabwareCreators::PlateSplitToTubeRacks} diff --git a/docs/state_changers.md b/docs/state_changers.md index 8235d005c..430446ab5 100644 --- a/docs/state_changers.md +++ b/docs/state_changers.md @@ -24,6 +24,16 @@ manual transfer. {StateChangers::DefaultStateChanger View class documentation} +## StateChangers::AutomaticTubeStateChanger + +{include:StateChangers::AutomaticTubeStateChanger} + + Used directly in 2 purposes: + LRC Bank Seq and LRC Bank Spare + +{StateChangers::AutomaticTubeStateChanger View class documentation} + + ## StateChangers::AutomaticPlateStateChanger {include:StateChangers::AutomaticPlateStateChanger} @@ -34,12 +44,21 @@ manual transfer. {StateChangers::AutomaticPlateStateChanger View class documentation} +## StateChangers::AutomaticLabwareStateChanger + +{include:StateChangers::AutomaticLabwareStateChanger} + + **This state changer is unused** + +{StateChangers::AutomaticLabwareStateChanger View class documentation} + + ## StateChangers::TubeStateChanger {include:StateChangers::TubeStateChanger} - Used directly in 49 purposes: - Cap Lib Pool Norm, CLCM DNA Pool, CLCM DNA Pool Norm, CLCM RNA Pool, CLCM RNA Pool Norm, GBS MiSeq Pool, GBS PCR Pool, GBS PCR Pool Selected, GBS PCR2 Pool Stock, GnT Pico Lib Pool, GnT Pico Lib Pool XP, LB Custom Pool, LB Custom Pool Norm, LB Lib Pool, LB Lib Pool Norm, LBB Lib Pool Stock, LBC 3pV3 GLibPS, LBC 5p GLibPS, LBC 5p Pool Norm, LBC BCR LibPS, LBC BCR Pool Norm, LBC TCR LibPS, LBC TCR Pool Norm, LBSN-384 PCR 2 Pool, LBSN-9216 Lib PCR Pool, LBSN-9216 Lib PCR Pool XP, LCA Bank Stock, LCA Blood Vac, LCA Custom Pool, LCA Custom Pool Norm, LCMB Custom Pool, LCMB Custom Pool Norm, LDS Custom Pool, LDS Custom Pool Norm, LHR Lib Pool, LHR Lib Pool XP, LHR-384 Pool XP, LRC Bank Seq, LRC Bank Spare, LRC Blood Aliquot, LRC Blood Vac, LTHR Pool XP, LTHR-384 Pool XP, LTN Custom Pool, LTN Custom Pool Norm, pWGS-384 Lib Pool XP, scRNA Lib Pool, scRNA Lib Pool XP, and scRNA-384 Lib Pool XP + Used directly in 47 purposes: + Cap Lib Pool Norm, CLCM DNA Pool, CLCM DNA Pool Norm, CLCM RNA Pool, CLCM RNA Pool Norm, GBS MiSeq Pool, GBS PCR Pool, GBS PCR Pool Selected, GBS PCR2 Pool Stock, GnT Pico Lib Pool, GnT Pico Lib Pool XP, LB Custom Pool, LB Custom Pool Norm, LB Lib Pool, LB Lib Pool Norm, LBB Lib Pool Stock, LBC 3pV3 GLibPS, LBC 5p GLibPS, LBC 5p Pool Norm, LBC BCR LibPS, LBC BCR Pool Norm, LBC TCR LibPS, LBC TCR Pool Norm, LBSN-384 PCR 2 Pool, LBSN-9216 Lib PCR Pool, LBSN-9216 Lib PCR Pool XP, LCA Bank Stock, LCA Blood Vac, LCA Custom Pool, LCA Custom Pool Norm, LCMB Custom Pool, LCMB Custom Pool Norm, LDS Custom Pool, LDS Custom Pool Norm, LHR Lib Pool, LHR Lib Pool XP, LHR-384 Pool XP, LRC Blood Aliquot, LRC Blood Vac, LTHR Pool XP, LTHR-384 Pool XP, LTN Custom Pool, LTN Custom Pool Norm, pWGS-384 Lib Pool XP, scRNA Lib Pool, scRNA Lib Pool XP, and scRNA-384 Lib Pool XP {StateChangers::TubeStateChanger View class documentation} diff --git a/spec/controllers/tubes/tubes_exports_controller_spec.rb b/spec/controllers/tubes/tubes_exports_controller_spec.rb index a4fc678fb..365f41e70 100644 --- a/spec/controllers/tubes/tubes_exports_controller_spec.rb +++ b/spec/controllers/tubes/tubes_exports_controller_spec.rb @@ -6,7 +6,8 @@ RSpec.describe Tubes::TubesExportsController, type: :controller do let(:tube_includes) do 'transfer_requests_as_target.source_asset,' \ - 'aliquots,aliquots.tag.tag_group,aliquots.tag2.tag_group,aliquots.sample.sample_metadata' + 'receptacle.aliquots,receptacle.aliquots.tag.tag_group,' \ + 'receptacle.aliquots.tag2.tag_group,receptacle.aliquots.sample.sample_metadata' end let(:tube_selects) do { 'aliquot' => %w[tag_index tag2_index], 'sample_metadata' => %w[supplier_name cohort sample_description] } diff --git a/spec/factories/purpose_config_factories.rb b/spec/factories/purpose_config_factories.rb index 54637186c..733069a7e 100644 --- a/spec/factories/purpose_config_factories.rb +++ b/spec/factories/purpose_config_factories.rb @@ -267,6 +267,19 @@ end end + factory :multi_stamp_tubes_using_tube_rack_scan_purpose_config do + creator_class do + { + name: 'LabwareCreators::MultiStampTubesUsingTubeRackScan', + args: { + expected_request_type_keys: ['parent_tube_library_request_type'], + expected_tube_purpose_names: ['Parent Tube Purpose Type 1', 'Parent Tube Purpose Type 2'], + filename_for_tube_rack_scan: 'tube_rack_scan.csv' + } + } + end + end + factory :donor_pooling_plate_purpose_config do transient { default_number_of_pools { 16 } } transient { max_number_of_source_plates { 2 } } diff --git a/spec/factories/receptacle_factories.rb b/spec/factories/receptacle_factories.rb index 9487e3d45..ac2c4016f 100644 --- a/spec/factories/receptacle_factories.rb +++ b/spec/factories/receptacle_factories.rb @@ -7,10 +7,15 @@ sequence(:id, &:to_s) uuid + requests_as_source { [] } + aliquots { [] } + transient { qc_results { [] } } after(:build) do |receptacle, evaluator| receptacle._cached_relationship(:qc_results) { evaluator.qc_results || [] } + receptacle._cached_relationship(:requests_as_source) { evaluator.requests_as_source || [] } + receptacle._cached_relationship(:aliquots) { evaluator.aliquots || [] } end end end diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb index 41858ad04..445adac03 100644 --- a/spec/factories/request_factories.rb +++ b/spec/factories/request_factories.rb @@ -70,6 +70,8 @@ factory :library_request do request_type { create :library_request_type } + after(:build) { |request, evaluator| request._cached_relationship(:request_type) { evaluator.request_type } } + # Library request with primer panel information factory :gbs_library_request do primer_panel diff --git a/spec/factories/tube_factories.rb b/spec/factories/tube_factories.rb index b80cd3b4b..c487968e6 100644 --- a/spec/factories/tube_factories.rb +++ b/spec/factories/tube_factories.rb @@ -98,7 +98,7 @@ state { 'passed' } purpose_name { 'example-purpose' } purpose_uuid { 'example-purpose-uuid' } - receptacle { create(:v2_receptacle, qc_results: []) } + receptacle { create(:v2_receptacle, qc_results: [], aliquots: aliquots) } created_at { '2017-06-29T09:31:59.000+01:00' } updated_at { '2017-06-29T09:31:59.000+01:00' } diff --git a/spec/fixtures/files/plate_split_to_tube_racks/test_file.txt b/spec/fixtures/files/common_file_handling/test_file.txt similarity index 100% rename from spec/fixtures/files/plate_split_to_tube_racks/test_file.txt rename to spec/fixtures/files/common_file_handling/test_file.txt diff --git a/spec/fixtures/files/tube_rack_scan_valid.csv b/spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_valid.csv similarity index 100% rename from spec/fixtures/files/tube_rack_scan_valid.csv rename to spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_valid.csv diff --git a/spec/fixtures/files/tube_rack_scan_with_bom.csv b/spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_bom.csv similarity index 100% rename from spec/fixtures/files/tube_rack_scan_with_bom.csv rename to spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_bom.csv diff --git a/spec/fixtures/files/tube_rack_scan_with_invalid_positions.csv b/spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_invalid_positions.csv similarity index 100% rename from spec/fixtures/files/tube_rack_scan_with_invalid_positions.csv rename to spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_invalid_positions.csv diff --git a/spec/fixtures/files/tube_rack_scan_with_missing_tubes.csv b/spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_missing_tubes.csv similarity index 100% rename from spec/fixtures/files/tube_rack_scan_with_missing_tubes.csv rename to spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_missing_tubes.csv diff --git a/spec/fixtures/files/tube_rack_scan_with_missing_values.csv b/spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_missing_values.csv similarity index 100% rename from spec/fixtures/files/tube_rack_scan_with_missing_values.csv rename to spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_missing_values.csv diff --git a/spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_valid.csv b/spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_valid.csv similarity index 100% rename from spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_valid.csv rename to spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_valid.csv diff --git a/spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_bom.csv b/spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_bom.csv similarity index 100% rename from spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_bom.csv rename to spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_bom.csv diff --git a/spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_different_rack_barcodes.csv b/spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_different_rack_barcodes.csv similarity index 100% rename from spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_different_rack_barcodes.csv rename to spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_different_rack_barcodes.csv diff --git a/spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_duplicate_well_positions.csv b/spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_duplicate_positions.csv similarity index 100% rename from spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_duplicate_well_positions.csv rename to spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_duplicate_positions.csv diff --git a/spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_duplicate_tubes.csv b/spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_duplicate_tubes.csv similarity index 100% rename from spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_duplicate_tubes.csv rename to spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_duplicate_tubes.csv diff --git a/spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_invalid_positions.csv b/spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_invalid_positions.csv similarity index 100% rename from spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_invalid_positions.csv rename to spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_invalid_positions.csv diff --git a/spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_missing_tubes.csv b/spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_missing_tubes.csv similarity index 100% rename from spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_missing_tubes.csv rename to spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_missing_tubes.csv diff --git a/spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_missing_values.csv b/spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_missing_values.csv similarity index 100% rename from spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_missing_values.csv rename to spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_missing_values.csv diff --git a/spec/fixtures/files/pooling_file.csv b/spec/fixtures/files/custom_pooled_tubes/pooling_file.csv similarity index 100% rename from spec/fixtures/files/pooling_file.csv rename to spec/fixtures/files/custom_pooled_tubes/pooling_file.csv diff --git a/spec/fixtures/files/pooling_file_with_bom.csv b/spec/fixtures/files/custom_pooled_tubes/pooling_file_with_bom.csv similarity index 100% rename from spec/fixtures/files/pooling_file_with_bom.csv rename to spec/fixtures/files/custom_pooled_tubes/pooling_file_with_bom.csv diff --git a/spec/fixtures/files/pooling_file_with_invalid_wells.csv b/spec/fixtures/files/custom_pooled_tubes/pooling_file_with_invalid_wells.csv similarity index 100% rename from spec/fixtures/files/pooling_file_with_invalid_wells.csv rename to spec/fixtures/files/custom_pooled_tubes/pooling_file_with_invalid_wells.csv diff --git a/spec/fixtures/files/pooling_file_with_zero_and_blank.csv b/spec/fixtures/files/custom_pooled_tubes/pooling_file_with_zero_and_blank.csv similarity index 100% rename from spec/fixtures/files/pooling_file_with_zero_and_blank.csv rename to spec/fixtures/files/custom_pooled_tubes/pooling_file_with_zero_and_blank.csv diff --git a/spec/fixtures/files/duplex_seq_dil_file.csv b/spec/fixtures/files/duplex_seq/duplex_seq_dil_file.csv similarity index 100% rename from spec/fixtures/files/duplex_seq_dil_file.csv rename to spec/fixtures/files/duplex_seq/duplex_seq_dil_file.csv diff --git a/spec/fixtures/files/duplex_seq_dil_file_with_bom.csv b/spec/fixtures/files/duplex_seq/duplex_seq_dil_file_with_bom.csv similarity index 100% rename from spec/fixtures/files/duplex_seq_dil_file_with_bom.csv rename to spec/fixtures/files/duplex_seq/duplex_seq_dil_file_with_bom.csv diff --git a/spec/fixtures/files/duplex_seq_dil_file_with_invalid_wells.csv b/spec/fixtures/files/duplex_seq/duplex_seq_dil_file_with_invalid_wells.csv similarity index 100% rename from spec/fixtures/files/duplex_seq_dil_file_with_invalid_wells.csv rename to spec/fixtures/files/duplex_seq/duplex_seq_dil_file_with_invalid_wells.csv diff --git a/spec/fixtures/files/duplex_seq_dil_file_with_missing_values.csv b/spec/fixtures/files/duplex_seq/duplex_seq_dil_file_with_missing_values.csv similarity index 100% rename from spec/fixtures/files/duplex_seq_dil_file_with_missing_values.csv rename to spec/fixtures/files/duplex_seq/duplex_seq_dil_file_with_missing_values.csv diff --git a/spec/fixtures/files/multi_stamp_tubes_using_tube_rack_scan/tube_rack_scan_valid.csv b/spec/fixtures/files/multi_stamp_tubes_using_tube_rack_scan/tube_rack_scan_valid.csv new file mode 100644 index 000000000..86cfc0dd4 --- /dev/null +++ b/spec/fixtures/files/multi_stamp_tubes_using_tube_rack_scan/tube_rack_scan_valid.csv @@ -0,0 +1,2 @@ +A1, AB10000001 +B1, AB10000002 \ No newline at end of file diff --git a/spec/fixtures/files/multi_stamp_tubes_using_tube_rack_scan/tube_rack_scan_with_unknown_barcode.csv b/spec/fixtures/files/multi_stamp_tubes_using_tube_rack_scan/tube_rack_scan_with_unknown_barcode.csv new file mode 100644 index 000000000..77bd39de5 --- /dev/null +++ b/spec/fixtures/files/multi_stamp_tubes_using_tube_rack_scan/tube_rack_scan_with_unknown_barcode.csv @@ -0,0 +1,3 @@ +A1, AB10000001 +B1, AB10000002 +C1, AB10000003 \ No newline at end of file diff --git a/spec/fixtures/files/scrna_core_contingency_tube_rack_scan.csv b/spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan.csv similarity index 100% rename from spec/fixtures/files/scrna_core_contingency_tube_rack_scan.csv rename to spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan.csv diff --git a/spec/fixtures/files/scrna_core_contingency_tube_rack_scan_2_tubes.csv b/spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan_2_tubes.csv similarity index 100% rename from spec/fixtures/files/scrna_core_contingency_tube_rack_scan_2_tubes.csv rename to spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan_2_tubes.csv diff --git a/spec/fixtures/files/scrna_core_contingency_tube_rack_scan_3_tubes.csv b/spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan_3_tubes.csv similarity index 100% rename from spec/fixtures/files/scrna_core_contingency_tube_rack_scan_3_tubes.csv rename to spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan_3_tubes.csv diff --git a/spec/fixtures/files/scrna_core_sequencing_tube_rack_scan.csv b/spec/fixtures/files/scrna_core/scrna_core_sequencing_tube_rack_scan.csv similarity index 100% rename from spec/fixtures/files/scrna_core_sequencing_tube_rack_scan.csv rename to spec/fixtures/files/scrna_core/scrna_core_sequencing_tube_rack_scan.csv diff --git a/spec/fixtures/files/scrna_core_sequencing_tube_rack_scan_duplicate_rack.csv b/spec/fixtures/files/scrna_core/scrna_core_sequencing_tube_rack_scan_duplicate_rack.csv similarity index 100% rename from spec/fixtures/files/scrna_core_sequencing_tube_rack_scan_duplicate_rack.csv rename to spec/fixtures/files/scrna_core/scrna_core_sequencing_tube_rack_scan_duplicate_rack.csv diff --git a/spec/fixtures/files/scrna_core_sequencing_tube_rack_scan_invalid.csv b/spec/fixtures/files/scrna_core/scrna_core_sequencing_tube_rack_scan_invalid.csv similarity index 100% rename from spec/fixtures/files/scrna_core_sequencing_tube_rack_scan_invalid.csv rename to spec/fixtures/files/scrna_core/scrna_core_sequencing_tube_rack_scan_invalid.csv diff --git a/spec/models/labware_creators/pooled_tubes_by_sample/csv_file_spec.rb b/spec/models/labware_creators/common_file_handling/csv_file_for_tube_rack_spec.rb similarity index 55% rename from spec/models/labware_creators/pooled_tubes_by_sample/csv_file_spec.rb rename to spec/models/labware_creators/common_file_handling/csv_file_for_tube_rack_spec.rb index f1c97a582..06258050c 100644 --- a/spec/models/labware_creators/pooled_tubes_by_sample/csv_file_spec.rb +++ b/spec/models/labware_creators/common_file_handling/csv_file_for_tube_rack_spec.rb @@ -1,64 +1,69 @@ # frozen_string_literal: true -RSpec.describe LabwareCreators::PooledTubesBySample::CsvFile, with: :uploader do - subject { described_class.new(file, 'DN2T') } +RSpec.describe LabwareCreators::CommonFileHandling::CsvFileForTubeRack, with: :uploader do + subject { described_class.new(file) } context 'Valid files' do let(:expected_position_details) do { 'A1' => { - 'barcode' => 'AB10000001' + 'tube_barcode' => 'AB10000001' }, 'B1' => { - 'barcode' => 'AB10000002' + 'tube_barcode' => 'AB10000002' }, 'C1' => { - 'barcode' => 'AB10000003' + 'tube_barcode' => 'AB10000003' }, 'D1' => { - 'barcode' => 'AB10000004' + 'tube_barcode' => 'AB10000004' }, 'E1' => { - 'barcode' => 'AB10000005' + 'tube_barcode' => 'AB10000005' }, 'F1' => { - 'barcode' => 'AB10000006' + 'tube_barcode' => 'AB10000006' }, 'G1' => { - 'barcode' => 'AB10000007' + 'tube_barcode' => 'AB10000007' }, 'H1' => { - 'barcode' => 'AB10000008' + 'tube_barcode' => 'AB10000008' }, 'A2' => { - 'barcode' => 'AB10000009' + 'tube_barcode' => 'AB10000009' }, 'B2' => { - 'barcode' => 'AB10000010' + 'tube_barcode' => 'AB10000010' }, 'C2' => { - 'barcode' => 'AB10000011' + 'tube_barcode' => 'AB10000011' }, 'D2' => { - 'barcode' => 'AB10000012' + 'tube_barcode' => 'AB10000012' }, 'E2' => { - 'barcode' => 'AB10000013' + 'tube_barcode' => 'AB10000013' }, 'F2' => { - 'barcode' => 'AB10000014' + 'tube_barcode' => 'AB10000014' }, 'G2' => { - 'barcode' => 'AB10000015' + 'tube_barcode' => 'AB10000015' }, 'H2' => { - 'barcode' => 'AB10000016' + 'tube_barcode' => 'AB10000016' } } end context 'Without byte order markers' do - let(:file) { fixture_file_upload('spec/fixtures/files/tube_rack_scan_valid.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_valid.csv', + 'sequencescape/qc_file' + ) + end describe '#valid?' do it 'should be valid' do @@ -74,7 +79,12 @@ end context 'With byte order markers' do - let(:file) { fixture_file_upload('spec/fixtures/files/tube_rack_scan_with_bom.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_bom.csv', + 'sequencescape/qc_file' + ) + end describe '#valid?' do it 'should be valid' do @@ -91,56 +101,59 @@ context 'A file which has missing tubes' do let(:file) do - fixture_file_upload('spec/fixtures/files/tube_rack_scan_with_missing_tubes.csv', 'sequencescape/qc_file') + fixture_file_upload( + 'spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_missing_tubes.csv', + 'sequencescape/qc_file' + ) end # missing tube rows should be filtered out e.g. C1 is a NO READ here let(:expected_position_details) do { 'A1' => { - 'barcode' => 'AB10000001' + 'tube_barcode' => 'AB10000001' }, 'B1' => { - 'barcode' => 'AB10000002' + 'tube_barcode' => 'AB10000002' }, 'D1' => { - 'barcode' => 'AB10000004' + 'tube_barcode' => 'AB10000004' }, 'E1' => { - 'barcode' => 'AB10000005' + 'tube_barcode' => 'AB10000005' }, 'F1' => { - 'barcode' => 'AB10000006' + 'tube_barcode' => 'AB10000006' }, 'G1' => { - 'barcode' => 'AB10000007' + 'tube_barcode' => 'AB10000007' }, 'H1' => { - 'barcode' => 'AB10000008' + 'tube_barcode' => 'AB10000008' }, 'A2' => { - 'barcode' => 'AB10000009' + 'tube_barcode' => 'AB10000009' }, 'B2' => { - 'barcode' => 'AB10000010' + 'tube_barcode' => 'AB10000010' }, 'C2' => { - 'barcode' => 'AB10000011' + 'tube_barcode' => 'AB10000011' }, 'D2' => { - 'barcode' => 'AB10000012' + 'tube_barcode' => 'AB10000012' }, 'E2' => { - 'barcode' => 'AB10000013' + 'tube_barcode' => 'AB10000013' }, 'F2' => { - 'barcode' => 'AB10000014' + 'tube_barcode' => 'AB10000014' }, 'G2' => { - 'barcode' => 'AB10000015' + 'tube_barcode' => 'AB10000015' }, 'H2' => { - 'barcode' => 'AB10000016' + 'tube_barcode' => 'AB10000016' } } end @@ -160,7 +173,12 @@ end context 'something that can not parse' do - let(:file) { fixture_file_upload('spec/fixtures/files/tube_rack_scan_valid.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_valid.csv', + 'sequencescape/qc_file' + ) + end before { allow(CSV).to receive(:parse).and_raise('Really bad file') } @@ -178,7 +196,10 @@ context 'A file which has missing values' do let(:file) do - fixture_file_upload('spec/fixtures/files/tube_rack_scan_with_missing_values.csv', 'sequencescape/qc_file') + fixture_file_upload( + 'spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_missing_values.csv', + 'sequencescape/qc_file' + ) end describe '#valid?' do @@ -186,11 +207,11 @@ expect(subject.valid?).to be false end - let(:row4_error) { 'Tube rack scan barcode cannot be empty, in row 4 [C1]' } + let(:row3_error) { 'Tube rack scan tube barcode cannot be empty, in row 3 [C1]' } it 'reports the errors' do subject.valid? - expect(subject.errors.full_messages).to include(row4_error) + expect(subject.errors.full_messages).to include(row3_error) end end end @@ -206,16 +227,16 @@ it 'reports the errors' do subject.valid? expect(subject.errors.full_messages).to include( - 'Tube rack scan position contains an invalid coordinate, in row 2 [THIS IS AN EXAMPLE FILE]' + 'Tube rack scan tube position contains an invalid coordinate, in row 1 [THIS IS AN EXAMPLE FILE]' ) expect(subject.errors.full_messages).to include( - 'Tube rack scan barcode cannot be empty, in row 2 [THIS IS AN EXAMPLE FILE]' + 'Tube rack scan tube barcode cannot be empty, in row 1 [THIS IS AN EXAMPLE FILE]' ) expect(subject.errors.full_messages).to include( - 'Tube rack scan position contains an invalid coordinate, in row 3 [IT IS USED TO TEST QC FILE UPLOAD]' + 'Tube rack scan tube position contains an invalid coordinate, in row 2 [IT IS USED TO TEST QC FILE UPLOAD]' ) expect(subject.errors.full_messages).to include( - 'Tube rack scan barcode cannot be empty, in row 3 [IT IS USED TO TEST QC FILE UPLOAD]' + 'Tube rack scan tube barcode cannot be empty, in row 2 [IT IS USED TO TEST QC FILE UPLOAD]' ) end end @@ -223,7 +244,10 @@ context 'An unrecognised tube position' do let(:file) do - fixture_file_upload('spec/fixtures/files/tube_rack_scan_with_invalid_positions.csv', 'sequencescape/qc_file') + fixture_file_upload( + 'spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_invalid_positions.csv', + 'sequencescape/qc_file' + ) end describe '#valid?' do @@ -234,7 +258,7 @@ it 'reports the errors' do subject.valid? expect(subject.errors.full_messages).to include( - 'Tube rack scan position contains an invalid coordinate, in row 10 [I1]' + 'Tube rack scan tube position contains an invalid coordinate, in row 9 [I1]' ) end end diff --git a/spec/models/labware_creators/plate_split_to_tube_racks/csv_file_spec.rb b/spec/models/labware_creators/common_file_handling/csv_file_for_tube_rack_with_rack_barcode_spec.rb similarity index 81% rename from spec/models/labware_creators/plate_split_to_tube_racks/csv_file_spec.rb rename to spec/models/labware_creators/common_file_handling/csv_file_for_tube_rack_with_rack_barcode_spec.rb index b4f59fe72..98f8c544d 100644 --- a/spec/models/labware_creators/plate_split_to_tube_racks/csv_file_spec.rb +++ b/spec/models/labware_creators/common_file_handling/csv_file_for_tube_rack_with_rack_barcode_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -RSpec.describe LabwareCreators::PlateSplitToTubeRacks::CsvFile, with: :uploader do +# Tests for common tube rack csv file handling +RSpec.describe LabwareCreators::CommonFileHandling::CsvFileForTubeRackWithRackBarcode, with: :uploader do subject { described_class.new(file) } context 'Valid files' do @@ -76,7 +77,7 @@ context 'Without byte order markers' do let(:file) do fixture_file_upload( - 'spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_valid.csv', + 'spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_valid.csv', 'sequencescape/qc_file' ) end @@ -88,7 +89,7 @@ end describe '#position_details' do - it 'should parse the expected well details' do + it 'should parse the expected position details' do expect(subject.position_details).to eq expected_position_details end end @@ -97,7 +98,7 @@ context 'With byte order markers' do let(:file) do fixture_file_upload( - 'spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_bom.csv', + 'spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_bom.csv', 'sequencescape/qc_file' ) end @@ -109,7 +110,7 @@ end describe '#position_details' do - it 'should parse the expected well details' do + it 'should parse the expected position details' do expect(subject.position_details).to eq expected_position_details end end @@ -118,7 +119,7 @@ context 'A file which has missing tubes' do let(:file) do fixture_file_upload( - 'spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_missing_tubes.csv', + 'spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_missing_tubes.csv', 'sequencescape/qc_file' ) end @@ -196,7 +197,7 @@ end describe '#position_details' do - it 'should parse the expected well details' do + it 'should parse the expected position details' do expect(subject.position_details).to eq expected_position_details end end @@ -206,7 +207,7 @@ context 'something that can not parse' do let(:file) do fixture_file_upload( - 'spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_valid.csv', + 'spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_valid.csv', 'sequencescape/qc_file' ) end @@ -228,7 +229,7 @@ context 'A file which has missing values' do let(:file) do fixture_file_upload( - 'spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_missing_values.csv', + 'spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_missing_values.csv', 'sequencescape/qc_file' ) end @@ -238,18 +239,18 @@ expect(subject.valid?).to be false end - let(:row4_error) { 'Tube rack scan tube barcode cannot be empty, in row 4 [C1]' } + let(:row3_error) { 'Tube rack scan tube barcode cannot be empty, in row 3 [C1]' } it 'reports the errors' do subject.valid? - expect(subject.errors.full_messages).to include(row4_error) + expect(subject.errors.full_messages).to include(row3_error) end end end context 'An invalid file' do let(:file) do - fixture_file_upload('spec/fixtures/files/plate_split_to_tube_racks/test_file.txt', 'sequencescape/qc_file') + fixture_file_upload('spec/fixtures/files/common_file_handling/test_file.txt', 'sequencescape/qc_file') end describe '#valid?' do @@ -261,19 +262,19 @@ subject.valid? expect(subject.errors.full_messages).to include( - 'Tube rack scan tube position contains an invalid coordinate, in row 2 [AN EXAMPLE FILE]' + 'Tube rack scan tube position contains an invalid coordinate, in row 1 [AN EXAMPLE FILE]' ) expect(subject.errors.full_messages).to include( - 'Tube rack scan tube barcode cannot be empty, in row 2 [AN EXAMPLE FILE]' + 'Tube rack scan tube barcode cannot be empty, in row 1 [AN EXAMPLE FILE]' ) expect(subject.errors.full_messages).to include( - 'Tube rack scan tube rack barcode cannot be empty, in row 3 [IT IS USED TO TEST QC FILE UPLOAD]' + 'Tube rack scan tube rack barcode cannot be empty, in row 2 [IT IS USED TO TEST QC FILE UPLOAD]' ) expect(subject.errors.full_messages).to include( - 'Tube rack scan tube position contains an invalid coordinate, in row 3 [IT IS USED TO TEST QC FILE UPLOAD]' + 'Tube rack scan tube position contains an invalid coordinate, in row 2 [IT IS USED TO TEST QC FILE UPLOAD]' ) expect(subject.errors.full_messages).to include( - 'Tube rack scan tube barcode cannot be empty, in row 3 [IT IS USED TO TEST QC FILE UPLOAD]' + 'Tube rack scan tube barcode cannot be empty, in row 2 [IT IS USED TO TEST QC FILE UPLOAD]' ) end end @@ -282,7 +283,8 @@ context 'An unrecognised tube position' do let(:file) do fixture_file_upload( - 'spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_invalid_positions.csv', + 'spec/fixtures/files/common_file_handling/' \ + 'tube_rack_with_rack_barcode/tube_rack_scan_with_invalid_positions.csv', 'sequencescape/qc_file' ) end @@ -295,7 +297,7 @@ it 'reports the errors' do subject.valid? expect(subject.errors.full_messages).to include( - 'Tube rack scan tube position contains an invalid coordinate, in row 10 [I1]' + 'Tube rack scan tube position contains an invalid coordinate, in row 9 [I1]' ) end end @@ -305,7 +307,8 @@ context 'A file with inconsistant rack barcodes' do let(:file) do fixture_file_upload( - 'spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_different_rack_barcodes.csv', + 'spec/fixtures/files/common_file_handling/' \ + 'tube_rack_with_rack_barcode/tube_rack_scan_with_different_rack_barcodes.csv', 'sequencescape/qc_file' ) end @@ -324,11 +327,12 @@ end end - # the same well position should not appear more than once in the file - context 'A file with duplicated well positions' do + # the same position should not appear more than once in the file + context 'A file with duplicated positions' do let(:file) do fixture_file_upload( - 'spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_duplicate_well_positions.csv', + 'spec/fixtures/files/common_file_handling/' \ + 'tube_rack_with_rack_barcode/tube_rack_scan_with_duplicate_positions.csv', 'sequencescape/qc_file' ) end @@ -340,7 +344,7 @@ it 'reports the errors' do subject.valid? - expect(subject.errors.full_messages).to include('contains duplicate well coordinates (A2,E2)') + expect(subject.errors.full_messages).to include('contains duplicate rack positions (A2,E2)') end end end @@ -349,7 +353,7 @@ context 'A file with duplicated tube barcodes' do let(:file) do fixture_file_upload( - 'spec/fixtures/files/plate_split_to_tube_racks/tube_rack_scan_with_duplicate_tubes.csv', + 'spec/fixtures/files/common_file_handling/tube_rack_with_rack_barcode/tube_rack_scan_with_duplicate_tubes.csv', 'sequencescape/qc_file' ) end diff --git a/spec/models/labware_creators/custom_pooled_tubes/csv_file_spec.rb b/spec/models/labware_creators/custom_pooled_tubes/csv_file_spec.rb index f820545a4..1bfaf8925 100644 --- a/spec/models/labware_creators/custom_pooled_tubes/csv_file_spec.rb +++ b/spec/models/labware_creators/custom_pooled_tubes/csv_file_spec.rb @@ -5,7 +5,9 @@ let(:expected_pools) { { '1' => %w[A1 B1 D1 E1 F1 G1 H1 A2 B2], '2' => %w[C1 C2 D2 E2 F2 G2] } } context 'Without byte order markers' do - let(:file) { fixture_file_upload('spec/fixtures/files/pooling_file.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload('spec/fixtures/files/custom_pooled_tubes/pooling_file.csv', 'sequencescape/qc_file') + end describe '#valid?' do subject { described_class.new(file) } @@ -21,7 +23,12 @@ end context 'With byte order markers' do - let(:file) { fixture_file_upload('spec/fixtures/files/pooling_file_with_bom.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/custom_pooled_tubes/pooling_file_with_bom.csv', + 'sequencescape/qc_file' + ) + end describe '#valid?' do subject { described_class.new(file) } @@ -38,7 +45,9 @@ end context 'something that can not parse' do - let(:file) { fixture_file_upload('spec/fixtures/files/pooling_file.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload('spec/fixtures/files/custom_pooled_tubes/pooling_file.csv', 'sequencescape/qc_file') + end before { allow(CSV).to receive(:parse).and_raise('Really bad file') } @@ -57,7 +66,10 @@ context 'A valid file with missing volumes' do let(:file) do - fixture_file_upload('spec/fixtures/files/pooling_file_with_zero_and_blank.csv', 'sequencescape/qc_file') + fixture_file_upload( + 'spec/fixtures/files/custom_pooled_tubes/pooling_file_with_zero_and_blank.csv', + 'sequencescape/qc_file' + ) end describe '#valid?' do @@ -105,7 +117,10 @@ context 'An unrecognised well' do let(:file) do - fixture_file_upload('spec/fixtures/files/pooling_file_with_invalid_wells.csv', 'sequencescape/qc_file') + fixture_file_upload( + 'spec/fixtures/files/custom_pooled_tubes/pooling_file_with_invalid_wells.csv', + 'sequencescape/qc_file' + ) end describe '#valid?' do diff --git a/spec/models/labware_creators/custom_pooled_tubes_spec.rb b/spec/models/labware_creators/custom_pooled_tubes_spec.rb index 3ab980db5..25a99be65 100644 --- a/spec/models/labware_creators/custom_pooled_tubes_spec.rb +++ b/spec/models/labware_creators/custom_pooled_tubes_spec.rb @@ -134,7 +134,9 @@ end context 'with a valid file' do - let(:file) { fixture_file_upload('spec/fixtures/files/pooling_file.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload('spec/fixtures/files/custom_pooled_tubes/pooling_file.csv', 'sequencescape/qc_file') + end it 'pools according to the file' do expect(subject.save).to be_truthy @@ -153,7 +155,9 @@ end context 'with empty wells includes' do - let(:file) { fixture_file_upload('spec/fixtures/files/pooling_file.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload('spec/fixtures/files/custom_pooled_tubes/pooling_file.csv', 'sequencescape/qc_file') + end let(:wells_json) { json :well_collection, size: 8 } it 'is false' do diff --git a/spec/models/labware_creators/multi_stamp_tubes_using_tube_rack_scan_spec.rb b/spec/models/labware_creators/multi_stamp_tubes_using_tube_rack_scan_spec.rb new file mode 100644 index 000000000..8b097209c --- /dev/null +++ b/spec/models/labware_creators/multi_stamp_tubes_using_tube_rack_scan_spec.rb @@ -0,0 +1,341 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'labware_creators/base' +require_relative 'shared_examples' + +# # Up to 96 tubes are transferred onto a single 96-well plate. +RSpec.describe LabwareCreators::MultiStampTubesUsingTubeRackScan, with: :uploader do + it_behaves_like 'it only allows creation from tubes' + + has_a_working_api + + # samples + let(:sample1_uuid) { SecureRandom.uuid } + let(:sample2_uuid) { SecureRandom.uuid } + + let(:sample1) { create(:v2_sample, name: 'Sample1', uuid: sample1_uuid) } + let(:sample2) { create(:v2_sample, name: 'Sample2', uuid: sample2_uuid) } + + # requests + let(:request_type_key) { 'parent_tube_library_request_type' } + + let(:request_type_1) { create :request_type, key: request_type_key } + let(:request_type_2) { create :request_type, key: request_type_key } + + let(:request_1) { create :library_request, request_type: request_type_1, uuid: 'request-1', submission_id: '1' } + let(:request_2) { create :library_request, request_type: request_type_2, uuid: 'request-2', submission_id: '1' } + + let(:ancestor_request_1) { create :request, uuid: 'ancestor-request-uuid' } + let(:ancestor_request_2) { create :request, uuid: 'ancestor-request-uuid' } + + # parent aliquots + # NB. in scRNA the outer request is an already passed request ie. from the earlier submission + let(:parent_tube_1_aliquot) { create(:v2_aliquot, sample: sample1, outer_request: ancestor_request_1) } + let(:parent_tube_2_aliquot) { create(:v2_aliquot, sample: sample2, outer_request: ancestor_request_2) } + + # receptacles + let(:receptacle_1) { create(:v2_receptacle, qc_results: [], requests_as_source: [request_1]) } + let(:receptacle_2) { create(:v2_receptacle, qc_results: [], requests_as_source: [request_2]) } + + # parent tube foreign barcodes (need to match values of foreign barcodes in csv file) + let(:parent_tube_1_foreign_barcode) { 'AB10000001' } + let(:parent_tube_2_foreign_barcode) { 'AB10000002' } + + # purpose uuids + let(:parent_tube_1_purpose_uuid) { 'parent-tube-purpose-type-1-uuid' } + let(:parent_tube_2_purpose_uuid) { 'parent-tube-purpose-type-2-uuid' } + + # purpose names + let(:parent_tube_1_purpose_name) { 'Parent Tube Purpose Type 1' } + let(:parent_tube_2_purpose_name) { 'Parent Tube Purpose Type 2' } + + # parent tubes + let(:parent_tube_1_uuid) { 'tube-1-uuid' } + + let(:parent_tube_1) do + create( + :v2_tube, + uuid: parent_tube_1_uuid, + purpose_uuid: parent_tube_1_purpose_uuid, + purpose_name: parent_tube_1_purpose_name, + aliquots: [parent_tube_1_aliquot], + receptacle: receptacle_1, + barcode_number: 1, + foreign_barcode: parent_tube_1_foreign_barcode + ) + end + + let(:parent_tube_2_uuid) { 'tube-2-uuid' } + let(:parent_tube_2) do + create( + :v2_tube, + uuid: parent_tube_2_uuid, + purpose_uuid: parent_tube_2_purpose_name, + purpose_name: parent_tube_2_purpose_name, + aliquots: [parent_tube_2_aliquot], + receptacle: receptacle_2, + barcode_number: 2, + foreign_barcode: parent_tube_2_foreign_barcode + ) + end + + let(:tube_includes) do + [:purpose, 'receptacle.aliquots.request.request_type', 'receptacle.requests_as_source.request_type'] + end + + # child aliquots + let(:child_aliquot1) { create :v2_aliquot } + let(:child_aliquot2) { create :v2_aliquot } + + # child wells + let(:child_well1) { create :v2_well, location: 'A1', uuid: '5-well-A1', aliquots: [child_aliquot1] } + let(:child_well2) { create :v2_well, location: 'B1', uuid: '5-well-B1', aliquots: [child_aliquot2] } + + # child plate + let(:child_plate_uuid) { 'child-uuid' } + let(:child_plate_purpose_uuid) { 'child-purpose' } + let(:child_plate_purpose_name) { 'Child Purpose' } + let(:child_plate_v2) do + create :v2_plate, + uuid: child_plate_uuid, + purpose_name: child_plate_purpose_name, + barcode_number: '5', + size: 96, + wells: [child_well1, child_well2] + end + + let(:child_plate_v2) do + create :v2_plate, uuid: child_plate_uuid, purpose_name: child_plate_purpose_name, barcode_number: '5', size: 96 + end + + let(:user_uuid) { 'user-uuid' } + let(:user) { json :v1_user, uuid: user_uuid } + + let!(:purpose_config) do + create :multi_stamp_tubes_using_tube_rack_scan_purpose_config, + name: child_plate_purpose_name, + uuid: child_plate_purpose_uuid + end + + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/multi_stamp_tubes_using_tube_rack_scan/tube_rack_scan_valid.csv', + 'sequencescape/qc_file' + ) + end + + let(:file_content) do + content = file.read + file.rewind + content + end + + let(:stub_upload_file_creation) do + stub_request(:post, api_url_for(child_plate_uuid, 'qc_files')) + .with( + body: file_content, + headers: { + 'Content-Type' => 'sequencescape/qc_file', + 'Content-Disposition' => 'form-data; filename="tube_rack_scan.csv"' + } + ) + .to_return( + status: 201, + body: json(:qc_file, filename: 'tube_rack_scan.csv'), + headers: { + 'content-type' => 'application/json' + } + ) + end + + let(:child_plate_v1) do + # qc_files are created through the API V1. The actions attribute for qcfiles is required by the API V1. + json :plate, uuid: child_plate_uuid, purpose_uuid: child_plate_purpose_uuid, qc_files_actions: %w[read create] + end + + before do + allow(Sequencescape::Api::V2::Tube).to receive(:find_by) + .with(barcode: 'AB10000001', includes: tube_includes) + .and_return(parent_tube_1) + allow(Sequencescape::Api::V2::Tube).to receive(:find_by) + .with(barcode: 'AB10000002', includes: tube_includes) + .and_return(parent_tube_2) + + stub_v2_plate(child_plate_v2, stub_search: false, custom_query: [:plate_with_wells, child_plate_v2.uuid]) + + stub_api_get(child_plate_uuid, body: child_plate_v1) + + stub_upload_file_creation + end + + context '#new' do + let(:form_attributes) { { purpose_uuid: child_plate_purpose_uuid, parent_uuid: parent_tube_1_uuid } } + + subject { LabwareCreators::MultiStampTubesUsingTubeRackScan.new(api, form_attributes) } + + it 'can be created' do + expect(subject).to be_a LabwareCreators::MultiStampTubesUsingTubeRackScan + end + + it 'renders the "multi_stamp_tubes_using_tube_rack_scan" page' do + expect(subject.page).to eq('multi_stamp_tubes_using_tube_rack_scan') + end + + it 'describes the purpose uuid' do + expect(subject.purpose_uuid).to eq(child_plate_purpose_uuid) + end + end + + context '#save when everything is valid' do + let(:form_attributes) do + { user_uuid: user_uuid, purpose_uuid: child_plate_purpose_uuid, parent_uuid: parent_tube_1_uuid, file: file } + end + + let!(:ms_plate_creation_request) do + stub_api_post( + 'pooled_plate_creations', + payload: { + pooled_plate_creation: { + user: user_uuid, + child_purpose: child_plate_purpose_uuid, + parents: [parent_tube_1_uuid, parent_tube_2_uuid] + } + }, + body: json(:plate_creation, child_plate_uuid: child_plate_uuid) + ) + end + + let(:transfer_requests) do + [ + { source_asset: 'tube-1-uuid', target_asset: '5-well-A1', outer_request: 'request-1' }, + { source_asset: 'tube-2-uuid', target_asset: '5-well-B1', outer_request: 'request-2' } + ] + end + + let!(:transfer_creation_request) do + stub_api_post( + 'transfer_request_collections', + payload: { + transfer_request_collection: { + user: user_uuid, + transfer_requests: transfer_requests + } + }, + body: '{}' + ) + end + + subject { LabwareCreators::MultiStampTubesUsingTubeRackScan.new(api, form_attributes) } + + it 'creates a plate!' do + subject.save + expect(ms_plate_creation_request).to have_been_made.once + expect(transfer_creation_request).to have_been_made.once + end + end + + context 'when a file is not correctly parsed' do + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/common_file_handling/tube_rack/tube_rack_scan_with_invalid_positions.csv', + 'sequencescape/qc_file' + ) + end + + let(:form_attributes) do + { user_uuid: user_uuid, purpose_uuid: child_plate_purpose_uuid, parent_uuid: parent_tube_1_uuid, file: file } + end + + subject { LabwareCreators::MultiStampTubesUsingTubeRackScan.new(api, form_attributes) } + + before { subject.validate } + + it 'does not call the validations' do + expect(subject).not_to be_valid + expect(subject).not_to receive(:tubes_must_exist_in_lims) + expect(subject.errors.full_messages).to include( + 'Csv file tube rack scan tube position contains an invalid coordinate, in row 9 [I1]' + ) + end + end + + context 'when a tube is not in LIMS' do + let(:file) do + fixture_file_upload( + 'spec/fixtures/files/multi_stamp_tubes_using_tube_rack_scan/tube_rack_scan_with_unknown_barcode.csv', + 'sequencescape/qc_file' + ) + end + + let(:form_attributes) do + { user_uuid: user_uuid, purpose_uuid: child_plate_purpose_uuid, parent_uuid: parent_tube_1_uuid, file: file } + end + + subject { LabwareCreators::MultiStampTubesUsingTubeRackScan.new(api, form_attributes) } + + before do + allow(Sequencescape::Api::V2::Tube).to receive(:find_by) + .with(barcode: 'AB10000003', includes: tube_includes) + .and_return(nil) + + subject.validate + end + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors.full_messages).to include( + 'Tube barcode AB10000003 not found in the LIMS. ' \ + 'Please check the tube barcodes in the scan file are valid tubes.' + ) + end + end + + context 'when a tube is not of expected purpose type' do + let(:form_attributes) do + { user_uuid: user_uuid, purpose_uuid: child_plate_purpose_uuid, parent_uuid: parent_tube_1_uuid, file: file } + end + + let(:parent_tube_2_purpose_uuid) { 'parent-tube-purpose-type-unknown-uuid' } + let(:parent_tube_2_purpose_name) { 'Parent Tube Purpose Type Unknown' } + + subject { LabwareCreators::MultiStampTubesUsingTubeRackScan.new(api, form_attributes) } + + before { subject.validate } + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors.full_messages).to include( + 'Tube barcode AB10000002 does not match to one of the expected tube purposes ' \ + '(one of type(s): Parent Tube Purpose Type 1, Parent Tube Purpose Type 2)' + ) + end + end + + context 'when a tube does not have an active request of the expected type' do + let(:form_attributes) do + { user_uuid: user_uuid, purpose_uuid: child_plate_purpose_uuid, parent_uuid: parent_tube_1_uuid, file: file } + end + + let(:request_type_2) { create :request_type, key: 'unrelated_request_type_key' } + + subject { LabwareCreators::MultiStampTubesUsingTubeRackScan.new(api, form_attributes) } + + before { subject.validate } + + it 'is not valid' do + expect(subject).not_to be_valid + expect(subject.errors.full_messages).to include( + 'Tube barcode AB10000002 does not have an expected active request ' \ + '(one of type(s): parent_tube_library_request_type)' + ) + end + + it 'raises an error if it reaches the code to fetch outer request' do + expect { subject.send(:source_tube_outer_request_uuid, parent_tube_2) }.to raise_error( + RuntimeError, + "No active request of expected type found for tube #{parent_tube_2.human_barcode}" + ) + end + end +end diff --git a/spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq_spec.rb b/spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq_spec.rb index 47b61f1f6..b2181b296 100644 --- a/spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq_spec.rb +++ b/spec/models/labware_creators/pcr_cycles_binned_plate/csv_file_for_duplex_seq_spec.rb @@ -125,7 +125,9 @@ end context 'Without byte order markers' do - let(:file) { fixture_file_upload('spec/fixtures/files/duplex_seq_dil_file.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload('spec/fixtures/files/duplex_seq/duplex_seq_dil_file.csv', 'sequencescape/qc_file') + end describe '#valid?' do it 'should be valid' do @@ -142,7 +144,7 @@ context 'With byte order markers' do let(:file) do - fixture_file_upload('spec/fixtures/files/duplex_seq_dil_file_with_bom.csv', 'sequencescape/qc_file') + fixture_file_upload('spec/fixtures/files/duplex_seq/duplex_seq_dil_file_with_bom.csv', 'sequencescape/qc_file') end describe '#valid?' do @@ -160,7 +162,9 @@ end context 'something that can not parse' do - let(:file) { fixture_file_upload('spec/fixtures/files/duplex_seq_dil_file.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload('spec/fixtures/files/duplex_seq/duplex_seq_dil_file.csv', 'sequencescape/qc_file') + end before { allow(CSV).to receive(:parse).and_raise('Really bad file') } @@ -178,7 +182,10 @@ context 'A file which has missing well values' do let(:file) do - fixture_file_upload('spec/fixtures/files/duplex_seq_dil_file_with_missing_values.csv', 'sequencescape/qc_file') + fixture_file_upload( + 'spec/fixtures/files/duplex_seq/duplex_seq_dil_file_with_missing_values.csv', + 'sequencescape/qc_file' + ) end describe '#valid?' do @@ -258,7 +265,10 @@ context 'An unrecognised well' do let(:file) do - fixture_file_upload('spec/fixtures/files/duplex_seq_dil_file_with_invalid_wells.csv', 'sequencescape/qc_file') + fixture_file_upload( + 'spec/fixtures/files/duplex_seq/duplex_seq_dil_file_with_invalid_wells.csv', + 'sequencescape/qc_file' + ) end describe '#valid?' do @@ -276,7 +286,9 @@ context 'A parent plate barcode that does not match' do subject { described_class.new(file, csv_file_config, 'DN1S') } - let(:file) { fixture_file_upload('spec/fixtures/files/duplex_seq_dil_file.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload('spec/fixtures/files/duplex_seq/duplex_seq_dil_file.csv', 'sequencescape/qc_file') + end describe '#valid?' do it 'should be invalid' do diff --git a/spec/models/labware_creators/pcr_cycles_binned_plate_for_duplex_seq_spec.rb b/spec/models/labware_creators/pcr_cycles_binned_plate_for_duplex_seq_spec.rb index 4861a9273..93054cb08 100644 --- a/spec/models/labware_creators/pcr_cycles_binned_plate_for_duplex_seq_spec.rb +++ b/spec/models/labware_creators/pcr_cycles_binned_plate_for_duplex_seq_spec.rb @@ -327,7 +327,9 @@ end context 'binning' do - let(:file) { fixture_file_upload('spec/fixtures/files/duplex_seq_dil_file.csv', 'sequencescape/qc_file') } + let(:file) do + fixture_file_upload('spec/fixtures/files/duplex_seq/duplex_seq_dil_file.csv', 'sequencescape/qc_file') + end let!(:plate_creation_request) do stub_api_post( 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 a61c9e204..f5dddc115 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 @@ -187,11 +187,17 @@ end let(:sequencing_file) do - fixture_file_upload('spec/fixtures/files/scrna_core_sequencing_tube_rack_scan.csv', 'sequencescape/qc_file') + fixture_file_upload( + 'spec/fixtures/files/scrna_core/scrna_core_sequencing_tube_rack_scan.csv', + 'sequencescape/qc_file' + ) end let(:contingency_file) do - fixture_file_upload('spec/fixtures/files/scrna_core_contingency_tube_rack_scan.csv', 'sequencescape/qc_file') + fixture_file_upload( + 'spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan.csv', + 'sequencescape/qc_file' + ) end before do @@ -398,7 +404,7 @@ let(:sequencing_file) do fixture_file_upload( - 'spec/fixtures/files/scrna_core_sequencing_tube_rack_scan_invalid.csv', + 'spec/fixtures/files/scrna_core/scrna_core_sequencing_tube_rack_scan_invalid.csv', 'sequencescape/qc_file' ) end @@ -409,7 +415,7 @@ expect(subject).not_to be_valid expect(subject).not_to receive(:check_tube_rack_barcodes_differ_between_files) expect(subject.errors.full_messages).to include( - 'Sequencing csv file tube rack scan tube position contains an invalid coordinate, in row 2 [AAAA1]' + 'Sequencing csv file tube rack scan tube position contains an invalid coordinate, in row 1 [AAAA1]' ) end end @@ -427,7 +433,7 @@ let(:contingency_file) do fixture_file_upload( - 'spec/fixtures/files/scrna_core_contingency_tube_rack_scan_3_tubes.csv', + 'spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan_3_tubes.csv', 'sequencescape/qc_file' ) end @@ -459,14 +465,14 @@ let(:sequencing_file) do fixture_file_upload( - 'spec/fixtures/files/scrna_core_sequencing_tube_rack_scan_duplicate_rack.csv', + 'spec/fixtures/files/scrna_core/scrna_core_sequencing_tube_rack_scan_duplicate_rack.csv', 'sequencescape/qc_file' ) end let(:contingency_file) do fixture_file_upload( - 'spec/fixtures/files/scrna_core_contingency_tube_rack_scan_3_tubes.csv', + 'spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan_3_tubes.csv', 'sequencescape/qc_file' ) end @@ -523,14 +529,14 @@ let(:sequencing_file) do fixture_file_upload( - 'spec/fixtures/files/scrna_core_sequencing_tube_rack_scan_invalid.csv', + 'spec/fixtures/files/scrna_core/scrna_core_sequencing_tube_rack_scan_invalid.csv', 'sequencescape/qc_file' ) end let(:contingency_file) do fixture_file_upload( - 'spec/fixtures/files/scrna_core_contingency_tube_rack_scan_3_tubes.csv', + 'spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan_3_tubes.csv', 'sequencescape/qc_file' ) end @@ -541,7 +547,7 @@ expect(subject).not_to be_valid expect(subject).not_to receive(:check_tube_barcodes_differ_between_files) expect(subject.errors.full_messages).to include( - 'Sequencing csv file tube rack scan tube position contains an invalid coordinate, in row 2 [AAAA1]' + 'Sequencing csv file tube rack scan tube position contains an invalid coordinate, in row 1 [AAAA1]' ) end end @@ -877,7 +883,7 @@ let(:contingency_file) do fixture_file_upload( - 'spec/fixtures/files/scrna_core_contingency_tube_rack_scan_3_tubes.csv', + 'spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan_3_tubes.csv', 'sequencescape/qc_file' ) end @@ -1013,7 +1019,7 @@ let(:contingency_file) do fixture_file_upload( - 'spec/fixtures/files/scrna_core_contingency_tube_rack_scan_2_tubes.csv', + 'spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan_2_tubes.csv', 'sequencescape/qc_file' ) end @@ -1052,7 +1058,10 @@ let(:sequencing_file) { nil } let(:contingency_file) do - fixture_file_upload('spec/fixtures/files/scrna_core_contingency_tube_rack_scan.csv', 'sequencescape/qc_file') + fixture_file_upload( + 'spec/fixtures/files/scrna_core/scrna_core_contingency_tube_rack_scan.csv', + 'sequencescape/qc_file' + ) end let(:form_attributes) do