Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DPL-914 scRNA create tube rack (plate) from banked pmbc tubes #1586

Merged
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ddca014
added automatic state changer for tubes
andrewsparkes Feb 16, 2024
fec6cdd
added csv file and row for tube rack scan
andrewsparkes Feb 16, 2024
3eee14f
added purpose config factory
andrewsparkes Feb 16, 2024
4f86957
added custom page to upload tube rack scan
andrewsparkes Feb 16, 2024
f2cac66
added config changes for purposes
andrewsparkes Feb 16, 2024
22084d2
refactor to make easier to read
andrewsparkes Feb 16, 2024
9f62936
refactor to make easier to read 2
andrewsparkes Feb 16, 2024
11579a0
added delegates
andrewsparkes Feb 16, 2024
64d3dff
added helper methods for tubes
andrewsparkes Feb 16, 2024
94af82f
added test csv files
andrewsparkes Feb 16, 2024
da72890
added relationship to request_type on v2 request
andrewsparkes Feb 16, 2024
42aaed0
added requests_as_source to factory
andrewsparkes Feb 16, 2024
95c9cda
refactor to match plate version
andrewsparkes Feb 16, 2024
bfbabb5
initial version of labware creator and partial test - wip
andrewsparkes Feb 16, 2024
5657947
doc updates
andrewsparkes Feb 16, 2024
1979d99
added cached relationships into factories
andrewsparkes Feb 26, 2024
4677de4
spec test files for multi stamp tubes tests
andrewsparkes Feb 26, 2024
bf50c49
added tests for the various validations
andrewsparkes Feb 26, 2024
3f17011
updated docs
andrewsparkes Feb 26, 2024
e822cae
merged in develop
andrewsparkes Feb 26, 2024
bad5a3b
fix tests
andrewsparkes Feb 26, 2024
4aa152b
removed unneeded relationship
andrewsparkes Feb 27, 2024
215bc5d
modified includes and has requests to fetch in progress requests
andrewsparkes Feb 27, 2024
9f9d755
update docs
andrewsparkes Feb 27, 2024
28cf1cb
modified test to use ancestor requests
andrewsparkes Feb 27, 2024
2bf79b0
fix codepilot change
andrewsparkes Feb 27, 2024
583370c
added class type plate
andrewsparkes Feb 27, 2024
075cf29
Merge branch 'develop' into dpl-914-scrna-create-tube-rack-banked-pmb…
andrewsparkes Feb 28, 2024
fa569e7
moved v2 aliquots into v2 receptacle
andrewsparkes Mar 1, 2024
82d4a80
WIP: file upload not working
andrewsparkes Mar 1, 2024
e69c18b
added missing requires and uploader
andrewsparkes Mar 1, 2024
d357dd4
Fix file uploading in test by adding API V1 actions for qc_files to c…
yoldas Mar 3, 2024
174f424
added test for runtime error check
andrewsparkes Mar 4, 2024
3c12d57
Update app/views/plate_creation/multi_stamp_tubes_using_tube_rack_sca…
andrewsparkes Mar 5, 2024
a40de67
refactored to reduce code duplication for tube rack scan files
andrewsparkes Mar 6, 2024
01456df
Merge branch 'dpl-914-scrna-create-tube-rack-banked-pmbc-tubes' of gi…
andrewsparkes Mar 6, 2024
d592564
removed unneeded method
andrewsparkes Mar 7, 2024
9a2c6d7
fixed comment
andrewsparkes Mar 7, 2024
3d20dd7
refactor of csv file upload handling
andrewsparkes Mar 8, 2024
a3f9b75
fix initialisations and file paths
andrewsparkes Mar 8, 2024
19a7545
Update app/models/labware_creators/common_file_handling/csv_file/row_…
andrewsparkes Mar 11, 2024
d383ea8
changes after feedback
andrewsparkes Mar 11, 2024
1f3329c
merged develop in
andrewsparkes Mar 11, 2024
2114559
Change tube.aliquots to tube.receptacle.aliquots in includes
yoldas Mar 18, 2024
47cad6a
Merge branch 'develop' into dpl-914-scrna-create-tube-rack-banked-pmb…
andrewsparkes Mar 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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'

Check warning on line 25 in app/models/labware_creators/common_file_handling/csv_file/row_base.rb

View check run for this annotation

Codecov / codecov/patch

app/models/labware_creators/common_file_handling/csv_file/row_base.rb#L25

Added line #L25 was not covered by tests
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'

Check warning on line 32 in app/models/labware_creators/common_file_handling/csv_file/row_base.rb

View check run for this annotation

Codecov / codecov/patch

app/models/labware_creators/common_file_handling/csv_file/row_base.rb#L32

Added line #L32 was not covered by tests
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
Original file line number Diff line number Diff line change
@@ -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'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error messages could flow better, I think, but I guess you are copy pasting from an existing class? What does it end up as, something like:

Tube barcode cannot be empty, in CsvFile::RowForTubeRack

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'in' part is the to_s method that gives you a row number reference (plus well coord if available)
e.g. Tube barcode cannot be empty, in row 11 [G5]


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
Original file line number Diff line number Diff line change
@@ -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
66 changes: 66 additions & 0 deletions app/models/labware_creators/common_file_handling/csv_file_base.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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<Row>] 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
andrewsparkes marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
@@ -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<Row>] 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
Loading
Loading