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 917 scrna faculty input submission #4067

Merged
merged 11 commits into from
Apr 4, 2024
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ config/cucumber.yml
config/database.yml
*.min.js
public/vite-*
.ruby-lsp/vendor
89 changes: 75 additions & 14 deletions app/uat_actions/uat_actions/generate_plates.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,18 @@ class UatActions::GeneratePlates < UatActions
form_field :well_count,
:number_field,
label: 'Well Count',
help: 'The number of occupied wells on each plate',
help: 'The number of occupied wells on each plate (locations will be randomised if less than full)',
options: {
minimum: 1
}
form_field :number_of_samples_in_each_well,
:number_field,
label: 'Number of samples in each well',
help: 'The number of samples to create in each well. Default is 1. Max 10.',
options: {
minimum: 1,
maximum: 10
}
form_field :study_name,
:select,
label: 'Study',
Expand All @@ -38,11 +46,13 @@ class UatActions::GeneratePlates < UatActions
select_options: %w[Column Row Random]

validate :well_count_smaller_than_plate_size
validates :number_of_samples_in_each_well, numericality: { greater_than: 0, only_integer: true, allow_blank: false }

def self.default
new(
plate_count: 1,
well_count: 96,
number_of_samples_in_each_well: 1,
study_name: UatActions::StaticRecords.study.name,
plate_purpose_name: PlatePurpose.stock_plate_purpose.name,
well_layout: 'Column'
Expand All @@ -69,21 +79,72 @@ def well_count_smaller_than_plate_size
false
end

# Ensures number of samples per occupied well is at least 1
def num_samples_per_well
@num_samples_per_well ||=
if number_of_samples_in_each_well.present? && number_of_samples_in_each_well.to_i.positive?
Copy link

Choose a reason for hiding this comment

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

UatActions::GeneratePlates#num_samples_per_well calls 'number_of_samples_in_each_well.to_i' 2 times

number_of_samples_in_each_well.to_i
else
1
end
end

# Constructs wells for the given plate.
# For each well in the plate, it creates the specified number of samples using the `create_sample` method.
# @param plate [Plate] the plate for which to construct wells
def construct_wells(plate)
wells(plate).each do |well|
sample_name = "sample_#{plate.human_barcode}_#{well.map.description}"
sample =
Sample.new(
name: sample_name,
sanger_sample_id: sample_name,
studies: [study],
sample_metadata_attributes: {
supplier_name: sample_name,
cohort: "Cohort#{plate.human_barcode}",
sample_description: "Description#{plate.human_barcode}"
}
)
sample.save!(validate: false)
num_samples_per_well.times { |sample_index| create_sample(plate, well, sample_index + 1) }
Copy link

Choose a reason for hiding this comment

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

UatActions::GeneratePlates#construct_wells contains iterators nested 2 deep

end
end

# Creates a new sample with a unique name based on the plate, well, and sample index.
# The sample is built using the `build_sample` method and saved using the `save_sample` method.
# If the sample fails to save due to an ActiveRecord::RecordInvalid error, the error message is
# added to the base errors.
# @param plate [Plate] the plate associated with the sample
# @param well [Well] the well associated with the sample
# @param sample_index [Integer] the index of the sample
# @raise [ActiveRecord::RecordInvalid] if the sample fails to save
def create_sample(plate, well, sample_index)
sample_name = "sample_#{sample_index}_#{plate.human_barcode}_#{well.map.description}"
sample = build_sample(sample_name, plate)
save_sample(sample, well, sample_index)
rescue ActiveRecord::RecordInvalid => e
Copy link

Choose a reason for hiding this comment

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

UatActions::GeneratePlates#create_sample has the variable name 'e'

errors.add(:base, "Failed to create sample: #{e.message}")
end

# Builds a new Sample object with the given name and associated plate.
# The sample's metadata attributes are also set, including the supplier name, cohort, and sample description.
# @param sample_name [String] the name of the sample, also used as the sanger_sample_id and supplier_name
# @param plate [Plate] the plate associated with the sample, its human_barcode is used in the cohort and
# sample_description
# @return [Sample] the newly built Sample object
def build_sample(sample_name, plate)
Sample.new(
name: sample_name,
sanger_sample_id: sample_name,
studies: [study],
sample_metadata_attributes: {
supplier_name: sample_name,
cohort: "Cohort#{plate.human_barcode}",
Copy link

Choose a reason for hiding this comment

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

UatActions::GeneratePlates#build_sample calls 'plate.human_barcode' 2 times

Copy link

Choose a reason for hiding this comment

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

UatActions::GeneratePlates#build_sample refers to 'plate' more than self (maybe move it to another class?)

sample_description: "SD-#{plate.human_barcode}"
}
)
end

# Saves the given sample and creates an aliquot in the specified well.
# If there are multiple samples in each well, the aliquot is created with a tag depth.
# @param sample [Sample] the sample to be saved
# @param well [Well] the well where the aliquot will be created
# @param sample_index [Integer] the index of the sample in the well, used as tag depth
# if there are multiple samples per well
def save_sample(sample, well, sample_index)
sample.save!(validate: false)

if num_samples_per_well > 1
well.aliquots.create!(sample: sample, study: study, tag_depth: sample_index)
Copy link

Choose a reason for hiding this comment

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

UatActions::GeneratePlates#save_sample calls 'well.aliquots' 2 times

else
well.aliquots.create!(sample: sample, study: study)
end
end
Expand Down
64 changes: 51 additions & 13 deletions app/uat_actions/uat_actions/test_submission.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ class UatActions::TestSubmission < UatActions # rubocop:todo Metrics/ClassLength
options: {
minimum: 1
}
form_field :number_of_samples_in_each_well,
:number_field,
label: 'Number of samples per occupied well',
help:
'Use this option to create wells containing a pool of multiple samples. Enter ' \
'the number of samples per well. All occupied wells will have this number of samples.' \
'Useful for a pipeline where pools of starting samples is required.' \
'Leave blank for 1 sample per well. Max 10 samples per well.',
options: {
minimum: 1,
maximum: 10
}
form_field :number_of_wells_to_submit,
:number_field,
label: 'Number of wells to submit',
Expand All @@ -80,14 +92,15 @@ class UatActions::TestSubmission < UatActions # rubocop:todo Metrics/ClassLength

validates :submission_template, presence: { message: 'could not be found' }
validates :number_of_wells_with_samples, numericality: { greater_than: 0, only_integer: true, allow_blank: true }
validates :number_of_samples_in_each_well, numericality: { greater_than: 0, only_integer: true, allow_blank: true }
validates :number_of_wells_to_submit, numericality: { greater_than: 0, only_integer: true, allow_blank: true }

#
# Returns a default copy of the UatAction which will be used to fill in the form
#
# @return [UatActions::TestSubmission] A default object for rendering a form
def self.default
new
new(number_of_samples_in_each_well: 1)
end

def self.compatible_submission_templates
Expand Down Expand Up @@ -121,6 +134,7 @@ def perform # rubocop:todo Metrics/AbcSize
report['primer_panel'] = order.request_options[:primer_panel_name] if order.request_options[:primer_panel_name]
.present?
report['number_of_wells_with_samples'] = labware.wells.with_aliquots.size
report['number_of_samples_in_each_well'] = labware.wells.with_aliquots.first.aliquots.size
report['number_of_wells_to_submit'] = assets.size
order.submission.built!
true
Expand Down Expand Up @@ -166,24 +180,48 @@ def labware
@labware ||= plate_barcode.blank? ? generate_plate : Plate.find_by_barcode(plate_barcode.strip)
end

def generate_plate # rubocop:todo Metrics/MethodLength
generator = UatActions::GeneratePlates.default
generator.plate_purpose_name = plate_purpose_name.presence || default_purpose_name

num_sample_wells = number_of_wells_with_samples.to_i
generator.well_count =
if num_sample_wells.zero?
# default option, create a full plate
96
# Ensures number of samples per occupied well is at least 1
def num_samples_per_well
@num_samples_per_well ||=
if number_of_samples_in_each_well.present? && number_of_samples_in_each_well.to_i.positive?
Copy link

Choose a reason for hiding this comment

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

UatActions::TestSubmission#num_samples_per_well calls 'number_of_samples_in_each_well.to_i' 2 times

number_of_samples_in_each_well.to_i
else
# take the number entered in the form
num_sample_wells
1
end
generator.well_layout = 'Random'
end

# Generates a new plate using a plate generator.
# The generator is set up with the appropriate parameters, then used to perform the plate generation.
# After the plate is generated, the barcode is retrieved.
# @return [Plate] the newly generated Plate object
def generate_plate
generator = setup_generator
generator.perform
Plate.find_by_barcode(generator.report['plate_0'])
end

# Sets up a plate generator with the appropriate parameters.
# The generator is created with default settings, then its attributes are set based on the current object's state.
# The plate purpose name is set to the plate_purpose_name entered by the user, or to the default purpose name if
# plate_purpose_name is not present.
# The well count is determined by the `determine_well_count` method.
# The well layout is set to 'Random'.
# The number of samples in each well is set to num_samples_per_well.
# @return [UatActions::GeneratePlates] the configured plate generator
def setup_generator
Copy link

Choose a reason for hiding this comment

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

UatActions::TestSubmission#setup_generator has approx 6 statements

generator = UatActions::GeneratePlates.default
generator.plate_purpose_name = plate_purpose_name.presence || default_purpose_name
generator.well_count = determine_well_count
generator.well_layout = 'Random'
generator.number_of_samples_in_each_well = num_samples_per_well
generator
end

def determine_well_count
num_sample_wells = number_of_wells_with_samples.to_i
num_sample_wells.zero? ? 96 : num_sample_wells
end

def order_request_options
default_request_options.merge(custom_request_options)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
# Most are defined in the Limber config, but the below also need to be defined in Sequencescape.
# They are in a separate file so it can be 'feature flagged off' until needed.
---
# The 'LRC PBMC Pools' purpose is controlled by Limber. However, it has been
# added here to create submission and request type records for scRNA Core cDNA
# Prep stage.
LRC PBMC Pools:
stock_plate: false
cherrypickable_target: false
# The 'LRC PBMC Pools Input' purpose is included here so that it's available for
# sample manifests.
# sample manifests, and also because it is an acceptable purpose for the scRNA Core cDNA Prep Input
# submission template.
LRC PBMC Pools Input:
input_plate: true
stock_plate: true
cherrypickable_target: false
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,18 @@ limber_scrna_core_cdna_prep_v2:
# - Chromium single cell 3 prime HT v3
# - Chromium single cell BCR HT
# - Chromium single cell TCR HT
limber_scrna_core_cdna_prep_input:
name: scRNA Core cDNA Prep Input
asset_type: Well
order: 1
request_class_name: IlluminaHtp::Requests::StdLibraryRequest
for_multiplexing: false
billable: true
product_line_name: Short Read
acceptable_purposes:
- LRC PBMC Pools Input
library_types:
- Chromium single cell 5 prime HT v2
# - Chromium single cell 3 prime HT v3
# - Chromium single cell BCR HT
# - Chromium single cell TCR HT
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
---
Limber-Htp - WGS - NovaSeq 6000 Paired end sequencing:
name: "Limber-Htp - WGS - NovaSeq 6000 Paired end sequencing"
submission_class_name: "LinearSubmission"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ Limber-Htp - scRNA Core cDNA Prep:
request_type_keys: ["limber_scrna_core_cdna_prep_v2"]
product_line_name: Short Read
product_catalogue_name: scRNA Core
Limber-Htp - scRNA Core cDNA Prep Input:
submission_class_name: "LinearSubmission"
related_records:
request_type_keys: ["limber_scrna_core_cdna_prep_input"]
product_line_name: Short Read
product_catalogue_name: scRNA Core
14 changes: 13 additions & 1 deletion spec/uat_actions/generate_plates_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
let(:plate_barcode_3) { build(:plate_barcode) }

context 'when creating a single plate' do
let(:num_samples_per_well) { 1 }
let(:parameters) do
{
plate_purpose_name: PlatePurpose.stock_plate_purpose.name,
plate_count: 1,
well_count: 1,
study_name: study.name,
well_layout: 'Column'
well_layout: 'Column',
number_of_samples_in_each_well: num_samples_per_well
}
end
let(:report) do
Expand All @@ -32,6 +34,16 @@
expect(uat_action.perform).to be true
expect(uat_action.report['plate_0']).to eq report['plate_0']
expect(Plate.find_by_barcode(report['plate_0']).wells.first.aliquots.first.study).to eq study
expect(Plate.find_by_barcode(report['plate_0']).wells.first.aliquots.size).to eq 1
end

context 'with multiple samples per well' do
let(:num_samples_per_well) { 4 }

it 'can be performed' do
expect(uat_action.perform).to be true
expect(Plate.find_by_barcode(report['plate_0']).wells.first.aliquots.size).to eq 4
end
end
end

Expand Down
10 changes: 10 additions & 0 deletions spec/uat_actions/test_submission_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@
expect(uat_action.report['number_of_wells_to_submit']).to be_a Integer
end
end

context 'with optional number of samples per well supplied' do
let(:parameters) { { submission_template_name: submission_template.name, number_of_samples_in_each_well: '2' } }

it 'can be performed' do
expect(uat_action.perform).to be true
expect(uat_action.report['plate_barcode_0']).to eq report['plate_barcode_0']
expect(uat_action.report['number_of_samples_in_each_well']).to be_a Integer
end
end
end

it 'returns a default' do
Expand Down
Loading