diff --git a/app/uat_actions/uat_actions/generate_plates.rb b/app/uat_actions/uat_actions/generate_plates.rb index cde3b44858..95b183e738 100644 --- a/app/uat_actions/uat_actions/generate_plates.rb +++ b/app/uat_actions/uat_actions/generate_plates.rb @@ -1,11 +1,16 @@ # frozen_string_literal: true # Will construct plates with well_count wells filled with samples +# rubocop:disable Metrics/ClassLength class UatActions::GeneratePlates < UatActions self.title = 'Generate plate' self.description = 'Generate plates in the selected study.' self.category = :generating_samples + ERROR_WELL_COUNT_EXCEEDS_PLATE_SIZE = "Well count of %s exceeds the plate size of %s for the plate purpose '%s'." + ERROR_PLATE_PURPOSE_DOES_NOT_EXIST = "Plate purpose '%s' does not exist." + ERROR_STUDY_DOES_NOT_EXIST = 'Study %s does not exist.' + form_field :plate_purpose_name, :select, label: 'Plate Purpose', @@ -45,8 +50,27 @@ class UatActions::GeneratePlates < UatActions help: 'The order in which wells are laid out. Affects where empty wells are located.', 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 } + validates :plate_purpose_name, presence: true + validates :plate_count, numericality: { greater_than: 0, smaller_than: 20, only_integer: true, allow_blank: false } + validates :well_count, numericality: { greater_than: 0, only_integer: true, allow_blank: false } + validates :number_of_samples_in_each_well, + numericality: { + greater_than: 0, + smaller_than: 20, + only_integer: true, + allow_blank: false + } + validates :study_name, presence: true + validates :well_layout, + presence: true, + inclusion: { + in: %w[Column Row Random], + message: 'must be Column, Row, or Random' + } + + validate :validate_plate_purpose_exists + validate :validate_well_count_is_smaller_than_plate_size + validate :validate_study_exists def self.default new( @@ -72,11 +96,38 @@ def perform private - def well_count_smaller_than_plate_size - return true if well_count.to_i <= plate_purpose.size + # Validates that the plate purpose exists for the selected plate purpose name. + # + # @return [void] + def validate_plate_purpose_exists + return if plate_purpose_name.blank? + return if PlatePurpose.exists?(name: plate_purpose_name) + + message = format(ERROR_PLATE_PURPOSE_DOES_NOT_EXIST, plate_purpose_name) + errors.add(:plate_purpose_name, message) + end + + # Validates that the well count is smaller than the plate size for the + # selected plate purpose. + # + # @return [void] + def validate_well_count_is_smaller_than_plate_size + return unless PlatePurpose.exists?(name: plate_purpose_name) + return if well_count.to_i <= plate_purpose.size + + message = format(ERROR_WELL_COUNT_EXCEEDS_PLATE_SIZE, well_count, plate_purpose.size, plate_purpose.name) + errors.add(:well_count, message) + end + + # Validates that the study exists for the selected study name. + # + # @return [void] + def validate_study_exists + return if study_name.blank? + return if Study.exists?(name: study_name) - errors.add(:well_count, "is larger than the size of a #{plate_purpose.name} (plate_purpose.size)") - false + message = format(ERROR_STUDY_DOES_NOT_EXIST, study_name) + errors.add(:study_name, message) end # Ensures number of samples per occupied well is at least 1 @@ -171,3 +222,4 @@ def plate_purpose Purpose.find_by!(name: plate_purpose_name) end end +# rubocop:enable Metrics/ClassLength diff --git a/app/uat_actions/uat_actions/generate_sample_manifest.rb b/app/uat_actions/uat_actions/generate_sample_manifest.rb index 593993d515..6f296bf438 100644 --- a/app/uat_actions/uat_actions/generate_sample_manifest.rb +++ b/app/uat_actions/uat_actions/generate_sample_manifest.rb @@ -6,6 +6,8 @@ class UatActions::GenerateSampleManifest < UatActions self.description = 'Generate sample manifest with the provided information.' self.category = :generating_samples + ERROR_TUBE_PURPOSE_DOES_NOT_EXIST = "Tube purpose '%s' does not exist." + form_field :study_name, :select, label: 'Study Name', @@ -41,6 +43,9 @@ class UatActions::GenerateSampleManifest < UatActions form_field :with_samples, :check_box, help: 'Create new samples for recipients?', label: 'With Samples?' + validates :tube_purpose_name, presence: true + validate :validate_tube_purpose_exists + def self.default new( study_name: UatActions::StaticRecords.study.name, @@ -59,6 +64,17 @@ def perform true end + # Validates that the tube purpose exists for the selected tube purpose name. + # + # @return [void] + def validate_tube_purpose_exists + return if tube_purpose_name.blank? # Already validated by presence + return if Tube::Purpose.exists?(name: tube_purpose_name) + + message = format(ERROR_TUBE_PURPOSE_DOES_NOT_EXIST, tube_purpose_name) + errors.add(:tube_purpose_name, message) + end + def create_sample_manifest SampleManifest.create!(study:, supplier:, asset_type:, count:, purpose:) end diff --git a/app/uat_actions/uat_actions/test_submission.rb b/app/uat_actions/uat_actions/test_submission.rb index d9fbece763..71b746afa5 100644 --- a/app/uat_actions/uat_actions/test_submission.rb +++ b/app/uat_actions/uat_actions/test_submission.rb @@ -11,6 +11,12 @@ class UatActions::TestSubmission < UatActions # rubocop:todo Metrics/ClassLength 'This may produce odd results for some pipelines.' self.category = :setup_and_test + ERROR_SUBMISSION_TEMPLATE_DOES_NOT_EXIST = "Submission template '%s' does not exist." + ERROR_PLATE_DOES_NOT_EXIST = 'Plate with barcode %s does not exist.' + ERROR_PLATE_PURPOSE_DOES_NOT_EXIST = "Plate purpose '%s' does not exist." + ERROR_LIBRARY_TYPE_DOES_NOT_EXIST = "Library type '%s' does not exist." + ERROR_PRIMER_PANEL_DOES_NOT_EXIST = "Primer panel '%s' does not exist." + # Form fields form_field :submission_template_name, :select, @@ -95,6 +101,12 @@ class UatActions::TestSubmission < UatActions # rubocop:todo Metrics/ClassLength 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 } + validate :validate_submission_template_exists + validate :validate_plate_exists + validate :validate_plate_purpose_exists + validate :validate_library_type_exists + validate :validate_primer_panel_exists + # # Returns a default copy of the UatAction which will be used to fill in the form # @@ -145,6 +157,71 @@ def perform # rubocop:todo Metrics/AbcSize private + # Validates that the submission template exists for the specified submission + # template name. + # + # @return [void] + def validate_submission_template_exists + return if submission_template_name.blank? # already validated by presence + return if SubmissionTemplate.exists?(name: submission_template_name) + + message = format(ERROR_SUBMISSION_TEMPLATE_DOES_NOT_EXIST, submission_template_name) + errors.add(:submission_template_name, message) + end + + # Validates that the plate exists for the specified plate barcode. It is + # is skipped if no barcode is provided, because a new plate is generated. + # + # @return [void] + def validate_plate_exists + return if plate_barcode.blank? # an appropriate plate will be generated + return if Plate.find_by_barcode(plate_barcode.strip).present? + + message = format(ERROR_PLATE_DOES_NOT_EXIST, plate_barcode) + errors.add(:plate_barcode, message) + end + + # Validates that the plate purpose exists for the specified plate purpose + # name. It will be skipped if a barcode specified. It will be also skipped + # when no purpose is provided because an appropriate purpose will be used when + # generating a new plate. + # + # @return [void] + def validate_plate_purpose_exists + return if plate_barcode.present? # takes precedence over plate purpose + return if plate_purpose_name.blank? # an appropriate purpose will be used + return if PlatePurpose.exists?(name: plate_purpose_name) + + message = format(ERROR_PLATE_PURPOSE_DOES_NOT_EXIST, plate_purpose_name) + errors.add(:plate_purpose_name, message) + end + + # Validates that the library type exists for the specified library type name. + # It is skipped if no library type name is provided because the first library + # type found will be used. + # + # return [void] + def validate_library_type_exists + return if library_type_name.blank? # first library type found will be used + return if LibraryType.exists?(name: library_type_name) + + message = format(ERROR_LIBRARY_TYPE_DOES_NOT_EXIST, library_type_name) + errors.add(:library_type_name, message) + end + + # Validates that the primer panel exists for the specified primer panel name. + # It is skipped if no primer panel name is provided because it is not + # applicable for the current submission template. + # + # return [void] + def validate_primer_panel_exists + return if primer_panel_name.blank? # not applicable for the template + return if PrimerPanel.exists?(name: primer_panel_name) + + message = format(ERROR_PRIMER_PANEL_DOES_NOT_EXIST, primer_panel_name) + errors.add(:primer_panel_name, message) + end + def submission_template @submission_template = SubmissionTemplate.find_by(name: submission_template_name) end diff --git a/app/uat_actions/uat_actions/tube_submission.rb b/app/uat_actions/uat_actions/tube_submission.rb index 5ec6643bb9..190a640704 100644 --- a/app/uat_actions/uat_actions/tube_submission.rb +++ b/app/uat_actions/uat_actions/tube_submission.rb @@ -3,11 +3,16 @@ # This UAT Action will generates a basic submission for tubes. Initially, it # has been designed for generating scRNA Core Donor Pooling and cDNA Prep # submissions on LRC Bank Seq/Spare tubes. +# rubocop:disable Metrics/ClassLength class UatActions::TubeSubmission < UatActions self.title = 'Tube submission' self.description = 'Generates a basic submission for tubes.' self.category = :setup_and_test + ERROR_SUBMISSION_TEMPLATE_DOES_NOT_EXIST = "Submission template '%s' does not exist." + ERROR_TUBES_DO_NOT_EXIST = 'Tubes with barcodes do not exist: %s' + ERROR_LIBRARY_TYPE_DOES_NOT_EXIST = "Library type '%s' does not exist." + form_field :submission_template_name, :select, label: 'Submission Template', @@ -56,6 +61,11 @@ class UatActions::TubeSubmission < UatActions } validates :submission_template, presence: { message: 'could not be found' } + validates :tube_barcodes, presence: true + + validate :validate_submission_template_exists + validate :validate_tubes_exist + validate :validate_library_type_exists # Returns a default copy of the UatAction which will be used to fill in the form # @@ -95,6 +105,46 @@ def perform true end + # Validates that the submission template exists for the specified submission + # template name. + # + # @return [void] + def validate_submission_template_exists + return if submission_template_name.blank? # already validated by presence + return if SubmissionTemplate.exists?(name: submission_template_name) + + message = format(ERROR_SUBMISSION_TEMPLATE_DOES_NOT_EXIST, submission_template_name) + errors.add(:submission_template_name, message) + end + + # Validates that the tubes exist for the specified barcodes. + # + # @return [void] + def validate_tubes_exist + return if tube_barcodes.blank? # already validated by presence + barcodes = + tube_barcodes + .gsub(/(\\[trfvn])+/, ' ') + .split + .select do |barcode| + Tube.find_by_barcode(barcode).blank? # not found + end + + message = format(ERROR_TUBES_DO_NOT_EXIST, barcodes.join(', ')) + errors.add(:tube_barcodes, message) + end + + # Validates that the library type exists for the specified library type name. + # + # return [void] + def validate_library_type_exists + return if library_type_name.blank? # optional + return if LibraryType.exists?(name: library_type_name) + + message = format(ERROR_LIBRARY_TYPE_DOES_NOT_EXIST, library_type_name) + errors.add(:library_type_name, message) + end + # Fills the report with the information from the submission # # @return [Void] @@ -191,3 +241,4 @@ def user UatActions::StaticRecords.user end end +# rubocop:enable Metrics/ClassLength diff --git a/spec/uat_actions/generate_plates_spec.rb b/spec/uat_actions/generate_plates_spec.rb index d9ac522268..f0e52131d5 100644 --- a/spec/uat_actions/generate_plates_spec.rb +++ b/spec/uat_actions/generate_plates_spec.rb @@ -83,4 +83,83 @@ it 'returns a default' do expect(described_class.default).to be_a described_class end + + describe '#valid?' do + let(:uat_action) { described_class.new(parameters) } + + describe '#validate_plate_purpose_exists' do + let(:parameters) { { plate_purpose_name: } } + let(:error_message) { format(described_class::ERROR_PLATE_PURPOSE_DOES_NOT_EXIST, plate_purpose_name) } + + context 'when the plate purpose does not exist' do + let(:plate_purpose_name) { 'Invalid Plate Purpose' } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:plate_purpose_name]).to include(error_message) + end + end + + context 'when the plate purpose exists' do + let(:plate) { create(:plate) } + let(:plate_purpose_name) { plate.purpose.name } + + it 'does not add the error message' do + uat_action.valid? # run validations + expect(uat_action.errors[:plate_purpose_name]).not_to include(error_message) + end + end + end + + describe '#validate_well_count_is_smaller_than_plate_size' do + let(:parameters) { { plate_purpose_name:, well_count: } } + let(:plate) { create(:plate, size: 96) } + let(:plate_purpose_name) { plate.purpose.name } + let(:error_message) do + format(described_class::ERROR_WELL_COUNT_EXCEEDS_PLATE_SIZE, well_count, plate.size, plate_purpose_name) + end + + context 'when the well count exceeds the plate size' do + let(:well_count) { plate.size + 1 } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:well_count]).to include(error_message) + end + end + + context 'when the well count is equal to the plate size' do + let(:well_count) { plate.size } + + it 'does not add the error message' do + uat_action.valid? # run validations + expect(uat_action.errors[:well_count]).not_to include(error_message) + end + end + end + + describe '#validate_study_exists' do + let(:parameters) { { study_name: } } + let(:error_message) { format(described_class::ERROR_STUDY_DOES_NOT_EXIST, study_name) } + + context 'when the study exists' do + let(:study) { create(:study) } + let(:study_name) { study.name } + + it 'does not add the error message' do + uat_action.valid? # run validations + expect(uat_action.errors[:study_name]).not_to include(error_message) + end + end + + context 'when the study does not exist' do + let(:study_name) { 'Invalid Study' } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:study_name]).to include(error_message) + end + end + end + end end diff --git a/spec/uat_actions/generate_sample_manifest_spec.rb b/spec/uat_actions/generate_sample_manifest_spec.rb index 44f5be3144..cb0f5b6d01 100644 --- a/spec/uat_actions/generate_sample_manifest_spec.rb +++ b/spec/uat_actions/generate_sample_manifest_spec.rb @@ -110,4 +110,32 @@ expect(described_class.default).to be_a described_class end end + + describe '#valid?' do + let(:uat_action) { described_class.new(parameters) } + + describe '#validate_tube_purpose_exists' do + let(:parameters) { { tube_purpose_name: } } + let(:error_message) { format(described_class::ERROR_TUBE_PURPOSE_DOES_NOT_EXIST, tube_purpose_name) } + + context 'when the tube purpose does not exist' do + let(:tube_purpose_name) { 'Invalid Tube Purpose' } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:tube_purpose_name]).to include(error_message) + end + end + + context 'when the tube purpose exists' do + let(:tube) { create(:tube) } # Tube to get the purpose from + let(:tube_purpose_name) { tube.purpose.name } + + it 'does not add the error message' do + uat_action.valid? # run validations + expect(uat_action.errors[:tube_purpose_name]).not_to include(error_message) + end + end + end + end end diff --git a/spec/uat_actions/test_submission_spec.rb b/spec/uat_actions/test_submission_spec.rb index 5230308d32..bdcdad80d1 100644 --- a/spec/uat_actions/test_submission_spec.rb +++ b/spec/uat_actions/test_submission_spec.rb @@ -102,4 +102,130 @@ it 'returns a default' do expect(described_class.default).to be_a described_class end + + describe '#valid?' do + let(:uat_action) { described_class.new(parameters) } + + describe '#validate_submission_template_exists' do + let(:parameters) { { submission_template_name: } } + let(:error_message) do + format(described_class::ERROR_SUBMISSION_TEMPLATE_DOES_NOT_EXIST, submission_template_name) + end + + context 'when the submission template does not exist' do + let(:submission_template_name) { 'Invalid Submission Template' } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:submission_template_name]).to include(error_message) + end + end + + context 'when the submission template exists' do + let(:submission_template) { create(:submission_template) } + let(:submission_template_name) { submission_template.name } + + it 'does not add the error message' do + uat_action.valid? # run validations + expect(uat_action.errors[:submission_template_name]).not_to include(error_message) + end + end + end + + describe '#validate_plate_exists' do + let(:parameters) { { plate_barcode: } } + let(:error_message) { format(described_class::ERROR_PLATE_DOES_NOT_EXIST, plate_barcode) } + + context 'when the plate does not exist' do + let(:plate_barcode) { 'Invalid Plate Barcode' } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:plate_barcode]).to include(error_message) + end + end + + context 'when the plate purpose exists' do + let(:plate) { create(:plate) } + let(:plate_barcode) { plate.human_barcode } + + it 'does not add the error message' do + uat_action.valid? # run validations + expect(uat_action.errors[:plate_barcode]).not_to include(error_message) + end + end + end + + describe '#validate_plate_purpose_exists' do + let(:parameters) { { plate_purpose_name: } } + let(:error_message) { format(described_class::ERROR_PLATE_PURPOSE_DOES_NOT_EXIST, plate_purpose_name) } + + context 'when the plate purpose does not exist' do + let(:plate_purpose_name) { 'Invalid Plate Purpose' } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:plate_purpose_name]).to include(error_message) + end + end + + context 'when the plate purpose exists' do + let(:plate) { create(:plate) } + let(:plate_purpose_name) { plate.purpose.name } + + it 'does not add the error message' do + uat_action.valid? # run validations + expect(uat_action.errors[:plate_purpose_name]).not_to include(error_message) + end + end + end + + describe '#validate_library_type_exists' do + let(:parameters) { { library_type_name: } } + let(:error_message) { format(described_class::ERROR_LIBRARY_TYPE_DOES_NOT_EXIST, library_type_name) } + + context 'when the library type does not exist' do + let(:library_type_name) { 'Invalid Library Type' } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:library_type_name]).to include(error_message) + end + end + + context 'when the library type exists' do + let(:library_type) { create(:library_type) } + let(:library_type_name) { library_type.name } + + it 'does not add the error message' do + uat_action.valid? # run validations + expect(uat_action.errors[:library_type_name]).not_to include(error_message) + end + end + end + + describe '#validate_primer_panel_exists' do + let(:parameters) { { primer_panel_name: } } + let(:error_message) { format(described_class::ERROR_PRIMER_PANEL_DOES_NOT_EXIST, primer_panel_name) } + + context 'when the primer panel does not exist' do + let(:primer_panel_name) { 'Invalid Primer Panel' } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:primer_panel_name]).to include(error_message) + end + end + + context 'when the primer panel exists' do + let(:primer_panel) { create(:primer_panel) } + let(:primer_panel_name) { primer_panel.name } + + it 'does not add the error message' do + uat_action.valid? # run validations + expect(uat_action.errors[:primer_panel_name]).not_to include(error_message) + end + end + end + end end diff --git a/spec/uat_actions/tube_submission_spec.rb b/spec/uat_actions/tube_submission_spec.rb index a48deb5efc..535d01eabe 100644 --- a/spec/uat_actions/tube_submission_spec.rb +++ b/spec/uat_actions/tube_submission_spec.rb @@ -78,4 +78,78 @@ it 'returns a default' do expect(described_class.default).to be_a described_class end + + describe '#valid?' do + let(:uat_action) { described_class.new(parameters) } + + describe '#validate_submission_template_exists' do + let(:parameters) { { submission_template_name: } } + let(:error_message) do + format(described_class::ERROR_SUBMISSION_TEMPLATE_DOES_NOT_EXIST, submission_template_name) + end + + context 'when the submission template does not exist' do + let(:submission_template_name) { 'Invalid Submission Template' } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:submission_template_name]).to include(error_message) + end + end + + context 'when the submission template exists' do + let(:submission_template) { create(:submission_template) } + let(:submission_template_name) { submission_template.name } + + it 'does not add the error message' do + uat_action.valid? # run validations + expect(uat_action.errors[:submission_template_name]).not_to include(error_message) + end + end + end + + describe '#validate_tubes_exist' do + let(:parameters) { { tube_barcodes: } } + let(:error_message) do + barcodes = tube_barcodes_array.select { |barcode| barcode.start_with?('INVALID') }.join(', ') + format(described_class::ERROR_TUBES_DO_NOT_EXIST, barcodes) + end + + context 'when the tubes do not exist' do + # Testing valid and invalid barcodes together. + let(:tubes) { [create(:sample_tube)] } + let(:tube_barcodes_array) { %w[INVALID-1 INVALID-2] + tubes.map(&:human_barcode) } + let(:tube_barcodes) { tube_barcodes_array.join('\n') } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:tube_barcodes]).to include(error_message) + end + end + end + + describe '#validate_library_type_exists' do + let(:parameters) { { library_type_name: } } + let(:error_message) { format(described_class::ERROR_LIBRARY_TYPE_DOES_NOT_EXIST, library_type_name) } + + context 'when the library type does not exist' do + let(:library_type_name) { 'Invalid Library Type' } + + it 'adds the error message' do + expect(uat_action.valid?).to be false + expect(uat_action.errors[:library_type_name]).to include(error_message) + end + end + + context 'when the library type exists' do + let(:library_type) { create(:library_type) } + let(:library_type_name) { library_type.name } + + it 'does not add the error message' do + uat_action.valid? # run validations + expect(uat_action.errors[:library_type_name]).not_to include(error_message) + end + end + end + end end