From aa0b7ce8de838a5205c54aa06842b6ca7b931432 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 20 Mar 2026 14:40:57 +0000 Subject: [PATCH 01/44] added Ultima P3 and P3 tag groups and adjusted sample sheet generator --- .../sample_sheet_generator.rb | 4 +- .../tag_groups/006_ultima_p3_and_p4.wip.yml | 201 ++++++++++++++++++ .../ultima_p3_and_p4.wip.yml | 8 + .../sample_sheet_generator_spec.rb | 81 ++++++- 4 files changed, 287 insertions(+), 7 deletions(-) create mode 100644 config/default_records/tag_groups/006_ultima_p3_and_p4.wip.yml create mode 100644 config/default_records/tag_layout_templates/ultima_p3_and_p4.wip.yml diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index 212b3738c4..2fc09674f3 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -40,7 +40,9 @@ class Generator # rubocop:disable Metrics/ClassLength # Index_Barcode_Num column, i.e. Z0001 or Z097. ULTIMA_TAG_GROUPS = { 'Ultima P1' => 1, - 'Ultima P2' => 2 + 'Ultima P2' => 2, + 'Ultima P3' => 3, + 'Ultima P4' => 4 }.freeze # Initializes the generator with the given batch. diff --git a/config/default_records/tag_groups/006_ultima_p3_and_p4.wip.yml b/config/default_records/tag_groups/006_ultima_p3_and_p4.wip.yml new file mode 100644 index 0000000000..cb24166ed9 --- /dev/null +++ b/config/default_records/tag_groups/006_ultima_p3_and_p4.wip.yml @@ -0,0 +1,201 @@ +Ultima P3: + visible: true + adapter_type_name: Ultima + tags: + 1: CTGCACATTGTAGAT + 2: CTCATGGTATATGAT + 3: CTGCATGCTGGCGTGAT + 4: CGGCACGATGCTGAT + 5: CTCTTCATGAGCATGAT + 6: CTCATGGAGCATATGAT + 7: CATACTGCCTATGAT + 8: CAATGCATCTATATGAT + 9: CGAGCACAATGCATGAT + 10: CGGCAGCAGACTGAT + 11: CAACATGCACATCTGAT + 12: CGTGCATGGCATCTGAT + 13: CTACAGCAATGTGAT + 14: CATCAGTCCTGCGAT + 15: CAAGCGACATGCGAT + 16: CATGCTCAATATCTGAT + 17: CGACTCATGCCTGAT + 18: CATGGCATACTGCTGAT + 19: CAATCGCATCGTGAT + 20: CGGCACTGCAGTGAT + 21: CGGATCGCATGCGAT + 22: CTATTGCTCTGCATGAT + 23: CATCTGCGGCACATGAT + 24: CATCGATGCAGAATGAT + 25: CACATGACCAGCGAT + 26: CAATGCTGTAGTGAT + 27: CGACATGCACCTGAT + 28: CGCAAGCATGTCGAT + 29: CATGCTCGGCTGCTGAT + 30: CAAGTCTGCTCAGAT + 31: CATCATTGATCATCGAT + 32: CACTCACAATGCATGAT + 33: CATGTGGCTATCATGAT + 34: CATGTATGCGCAATGAT + 35: CTTGCGCTCTCAGAT + 36: CGCGCACAATATGAT + 37: CAGCGCACCAGCATGAT + 38: CTGCTATCTGGTGAT + 39: CGATATCATGGTGAT + 40: CATGCATGTGGATAGAT + 41: CTCTGCTCCTGCGAT + 42: CATCGCACCAGAGAT + 43: CAATCTGATATGATGAT + 44: CATACTGCAGGAGAT + 45: CTTCATGACAGAGAT + 46: CTACATCATGGCATGAT + 47: CGCTGTTGCATGATGAT + 48: CTTCACTCATGAGAT + 49: CATGCGCTTACAGAT + 50: CAGCATCTCTGCCTGAT + 51: CACTGCCATATCATGAT + 52: CTCAGCTGCGGCGAT + 53: CATGCAGAGCGCCTGAT + 54: CAATGCTATCGAGAT + 55: CAGATAAGAGCAGAT + 56: CAGAGCCACTCAGAT + 57: CATGTAATATCAGAT + 58: CAGCGCATGATCCTGAT + 59: CATAGATAGCCAGAT + 60: CACTAGCATGGCGAT + 61: CAATAGCATCGAGAT + 62: CTTCTGTGAGATGAT + 63: CTCTGCTCATTCATGAT + 64: CACAATAGCGATGAT + 65: CTGTAGGCATCTGAT + 66: CAACAGCTCTCTGAT + 67: CAGACACAATATGAT + 68: CTAGCAAGATCAGAT + 69: CTTGAGACATGAGAT + 70: CTCAGATGTCCAGAT + 71: CAGCCGCATGCACAGAT + 72: CTCGCATGTGGAGAT + 73: CACATGCATGTCCTGAT + 74: CGCGGCTGATATGAT + 75: CTGTGACAAGCTGAT + 76: CATGCGAGCATGGTGAT + 77: CAATACATGATCGAT + 78: CATGGACATATGCAGAT + 79: CAGCGCGCTGGCATGAT + 80: CATGCGTCACCTGAT + 81: CTGCATGTTGCTGAGAT + 82: CATCCTGCGATAGAT + 83: CAATATCTGAGTGAT + 84: CTGTGCAGGAGTGAT + 85: CGATGTGCCATAGAT + 86: CTCACATGGAGAGAT + 87: CAGAACATGTCAGAT + 88: CGCACAATGCGAGAT + 89: CATGCACAGAATGTGAT + 90: CGTATGGCAGCTGAT + 91: CTGATGGTGCTCATGAT + 92: CTCAGCCATGCGATGAT + 93: CATGCACAAGATCAGAT + 94: CTCTGTTGCTCAGAT + 95: CACAGCGCTCCAGAT + 96: CACAGAAGATGCGAT + +Ultima P4: + visible: true + adapter_type_name: Ultima + tags: + 1: CATCATGCTCCGCTGAT + 2: CTTGCTATGAGCGAT + 3: CATGTATCAGGTGAT + 4: CATGCGCAATGTATGAT + 5: CATGTGAGCGGTGAT + 6: CGCACAAGTCATGAT + 7: CATGCAATACATGAGAT + 8: CATCTATCAGGCGAT + 9: CGAGCATCAGGTGAT + 10: CAAGACTGATATGAT + 11: CATGCGGAGTGAGAT + 12: CAGATATAGCCTGAT + 13: CAAGAGAGAGCTGAT + 14: CTATCAAGCTCAGAT + 15: CAGAATACATGCGAT + 16: CTCAAGCGCACAGAT + 17: CACACACAACATGAT + 18: CTTACATGTCATGAT + 19: CAGAAGAGATATGAT + 20: CAGAGCGCCGCAGAT + 21: CGCTCAACATGCGAT + 22: CATGACAGTAATGAT + 23: CATGCAATCACATCGAT + 24: CAGTTGATACATGAT + 25: CTGTGACATAATGAT + 26: CATAGTGCTGCAATGAT + 27: CACGAGCGCAATGAT + 28: CACAATGCAGATGTGAT + 29: CTGCGCAGCATCCTGAT + 30: CAAGACATCAGAGAT + 31: CATGCTTGTGTCATGAT + 32: CAGTGCGACAATGAT + 33: CATATGGTGACAGAT + 34: CAATGATAGTGTGAT + 35: CATAGCAGCAACATGAT + 36: CATGACACCAGTGAT + 37: CAATCTCAGCAGATGAT + 38: CTCGATGAGCCTGAT + 39: CAACACTCTGCAGAT + 40: CTTAGCATCACTGAT + 41: CTGTTGTGATCAGAT + 42: CAAGTGTGCGCAGAT + 43: CATGAGCATGGTCTGAT + 44: CACTGATGCATGGAGAT + 45: CTTATATCTCATGAT + 46: CATCAGCTGAAGATGAT + 47: CACACATCCAGCGAT + 48: CAACATGTGATCGAT + 49: CGGCTATGCATCGAT + 50: CAAGTGAGCTGCGAT + 51: CAATAGACAGCTGAT + 52: CAGTCTGATCCTGAT + 53: CTTGCATAGCGTGAT + 54: CTCGTGGCATGCATGAT + 55: CGCTGCCTGCTGCTGAT + 56: CATAGTTCATCTGAT + 57: CAATCAGCAGACGAT + 58: CATGAGCATGAGGCGAT + 59: CACATCAGTGGCGAT + 60: CTGCATGCACACCTGAT + 61: CTACTGGCAGCTGAT + 62: CTATTGATATGAGAT + 63: CTCATAGCCTGCATGAT + 64: CACATGCTCAATGCGAT + 65: CAACTCATATGCGAT + 66: CATGGCTGTCTCGAT + 67: CATCTAGCCATAGAT + 68: CAGTCAACAGCAGAT + 69: CTAGCGCAATGAGAT + 70: CATCATTAGCGCGAT + 71: CGCTTCATGCTGCAGAT + 72: CTGATGGCGCGCATGAT + 73: CTGATCTATGCAATGAT + 74: CACTGAATGTGCGAT + 75: CGCAATCTATCTGAT + 76: CGGCATGCTGTAGAT + 77: CTCATGTGTGGTGAT + 78: CTGCATATTGTGATGAT + 79: CGCATGTATGGCGAT + 80: CATATAACATGCATGAT + 81: CATATCTCCTGTGAT + 82: CTGATACAGAATGAT + 83: CTGTGCGCCATAGAT + 84: CAGAGCATGCTAATGAT + 85: CATAAGATGCGTGAT + 86: CATACAAGAGCTGAT + 87: CATGCTGAATGTGTGAT + 88: CTCATGATTACTGAT + 89: CATGACGCATTAGAT + 90: CGTGTGCAACATGAT + 91: CGGTGCATATCAGAT + 92: CTTCGATGCTGTGAT + 93: CAATAGATGTGAGAT + 94: CGATGCCATCTCATGAT + 95: CGATCACAAGCTGAT + 96: CTGTGATGTAATGAT diff --git a/config/default_records/tag_layout_templates/ultima_p3_and_p4.wip.yml b/config/default_records/tag_layout_templates/ultima_p3_and_p4.wip.yml new file mode 100644 index 0000000000..72211085a2 --- /dev/null +++ b/config/default_records/tag_layout_templates/ultima_p3_and_p4.wip.yml @@ -0,0 +1,8 @@ +Ultima P3: + :tag_group_name: Ultima P3 + :direction_algorithm: TagLayout::InColumns + :walking_algorithm: TagLayout::WalkWellsOfPlate +Ultima P4: + :tag_group_name: Ultima P4 + :direction_algorithm: TagLayout::InColumns + :walking_algorithm: TagLayout::WalkWellsOfPlate diff --git a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb index 923a12a490..419fdfc3f7 100644 --- a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb +++ b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb @@ -53,6 +53,8 @@ # First oligo sequences for the two tag groups. let(:plate1_first_oligo) { 'CAGCTCGAATGCGAT' } let(:plate2_first_oligo) { 'CAGTCAGTTGCAGAT' } + let(:plate3_first_oligo) { 'CTGCACATTGTAGAT' } + let(:plate4_first_oligo) { 'CATCATGCTCCGCTGAT' } # Eagerly create tag groups and tags to get consistent IDs. let!(:tag_group1) do @@ -67,14 +69,28 @@ tg.tags.first.update!(oligo: plate2_first_oligo) end end - let(:tag_groups) { [tag_group1, tag_group2] } + let!(:tag_group3) do + create(:tag_group_with_tags, tag_count: 96, name: 'Ultima P3').tap do |tg| + # To test Z0193 matching with the oligo sequence. + tg.tags.first.update!(oligo: plate3_first_oligo) + end + end + let!(:tag_group4) do + create(:tag_group_with_tags, tag_count: 96, name: 'Ultima P4').tap do |tg| + # To test Z0289 matching with the oligo sequence. + tg.tags.first.update!(oligo: plate4_first_oligo) + end + end + let(:tag_groups) { [tag_group1, tag_group2, tag_group3, tag_group4] } let(:request_type) { create(:ultima_sequencing) } let(:pipeline) { create(:ultima_sequencing_pipeline, request_types: [request_type]) } let(:batch) { create(:ultima_sequencing_batch, pipeline:, requests:) } - let(:requests) { [request1, request2] } + let(:requests) { [request1, request2, request3, request4] } let(:request1) { create(:ultima_sequencing_request, asset: tube1.receptacle, request_type: request_type) } let(:request2) { create(:ultima_sequencing_request, asset: tube2.receptacle, request_type: request_type) } + let(:request3) { create(:ultima_sequencing_request, asset: tube3.receptacle, request_type: request_type) } + let(:request4) { create(:ultima_sequencing_request, asset: tube4.receptacle, request_type: request_type) } # Eagerly create tubes with aliquots to get consistent IDs. let!(:tube1) do @@ -91,9 +107,23 @@ create(:event, content: Time.zone.today.to_s, message: 'scanned in', family: 'scanned_into_lab', eventful: tube) tube end + let!(:tube3) do + receptacle = create(:receptacle) + tag_group3.tags.first(3).map { |tag| create(:aliquot, tag:, receptacle:) } + tube = create(:multiplexed_library_tube, receptacle:) + create(:event, content: Time.zone.today.to_s, message: 'scanned in', family: 'scanned_into_lab', eventful: tube) + tube + end + let!(:tube4) do + receptacle = create(:receptacle) + tag_group4.tags.first(3).map { |tag| create(:aliquot, tag:, receptacle:) } + tube = create(:multiplexed_library_tube, receptacle:) + create(:event, content: Time.zone.today.to_s, message: 'scanned in', family: 'scanned_into_lab', eventful: tube) + tube + end # Expected mapping of tag groups to their respective 1-based plate numbers. - let(:tag_group_index_map) { { tag_group1 => 1, tag_group2 => 2 } } + let(:tag_group_index_map) { { tag_group1 => 1, tag_group2 => 2, tag_group3 => 3, tag_group4 => 4 } } # Expected mapping of tags to their respective 1-based index numbers. let(:tag_index_map) do @@ -169,6 +199,16 @@ def map_description(map_id) csv = "#{request2.id_wafer_lims}.csv" "#{folder}/#{csv}" end + let(:zip_entry3_name) do + folder = "batch_#{batch.id}_sample_sheets" + csv = "#{request3.id_wafer_lims}.csv" + "#{folder}/#{csv}" + end + let(:zip_entry4_name) do + folder = "batch_#{batch.id}_sample_sheets" + csv = "#{request4.id_wafer_lims}.csv" + "#{folder}/#{csv}" + end # Expected CSV section headers from Zip; to peek at the content. let(:zip_content1_header) do "[Header],,,,,,,\r\nBatch #{batch.id} #{tube1.human_barcode},,,,,,,\r\n" @@ -176,12 +216,20 @@ def map_description(map_id) let(:zip_content2_header) do "[Header],,,,,,,\r\nBatch #{batch.id} #{tube2.human_barcode},,,,,,,\r\n" end + let(:zip_content3_header) do + "[Header],,,,,,,\r\nBatch #{batch.id} #{tube3.human_barcode},,,,,,,\r\n" + end + let(:zip_content4_header) do + "[Header],,,,,,,\r\nBatch #{batch.id} #{tube4.human_barcode},,,,,,,\r\n" + end it 'generates valid zip entries' do # Test: The sample manifest (csv file) is generated on user request per pool. # Test: The name should be uniquely identifiable (file name : batchId_NT_number) zip_hash = extract_zip(described_class.generate(batch)) - expect(zip_hash.keys).to contain_exactly(zip_entry1_name, zip_entry2_name) + expect(zip_hash.keys).to contain_exactly( + zip_entry1_name, zip_entry2_name, zip_entry3_name, zip_entry4_name + ) end it 'generates valid zip contents' do @@ -189,7 +237,9 @@ def map_description(map_id) zip_hash = extract_zip(described_class.generate(batch)) expect(zip_hash.values).to contain_exactly( a_string_including(zip_content1_header), - a_string_including(zip_content2_header) + a_string_including(zip_content2_header), + a_string_including(zip_content3_header), + a_string_including(zip_content4_header) ) end @@ -210,19 +260,26 @@ def map_description(map_id) # Parse the generated CSV for the tubes into rows and columns. let(:csv1) { CSV.parse(generator.csv_string(request1), row_sep: "\r\n", nil_value: '') } let(:csv2) { CSV.parse(generator.csv_string(request2), row_sep: "\r\n", nil_value: '') } + let(:csv3) { CSV.parse(generator.csv_string(request3), row_sep: "\r\n", nil_value: '') } + let(:csv4) { CSV.parse(generator.csv_string(request4), row_sep: "\r\n", nil_value: '') } # Test: Adding study_id column to the existing column (study_id per sample) # Expected sample rows let(:csv1_samples) { csv_samples_for(request1) } - let(:csv2_samples) { csv_samples_for(request2) } + let(:csv3_samples) { csv_samples_for(request3) } + let(:csv4_samples) { csv_samples_for(request4) } it 'generates header sections' do # rubocop:disable RSpec/MultipleExpectations expect(csv1[0].compact_blank).to eq(generator.class::HEADER_TITLE) expect(csv1[1].compact_blank).to eq(["Batch #{batch.id} #{tube1.human_barcode}"]) # First CSV expect(csv1[2].compact_blank).to eq([]) expect(csv2[1].compact_blank).to eq(["Batch #{batch.id} #{tube2.human_barcode}"]) # Second CSV + expect(csv2[2].compact_blank).to eq([]) + expect(csv3[1].compact_blank).to eq(["Batch #{batch.id} #{tube3.human_barcode}"]) # Third CSV + expect(csv3[2].compact_blank).to eq([]) + expect(csv4[1].compact_blank).to eq(["Batch #{batch.id} #{tube4.human_barcode}"]) end it 'generates global sections' do @@ -239,6 +296,8 @@ def map_description(map_id) expect(csv1[8].compact_blank).to eq(generator.class::SAMPLES_HEADERS) expect(csv1[9..]).to eq(csv1_samples) # First CSV expect(csv2[9..]).to eq(csv2_samples) # Second CSV + expect(csv3[9..]).to eq(csv3_samples) # Third CSV + expect(csv4[9..]).to eq(csv4_samples) # Fourth CSV end it 'matches the z-indexes, oligo sequences, and plate numbers' do @@ -251,6 +310,16 @@ def map_description(map_id) expect(csv2[9][2]).to eq('Z0097') # Index_Barcode_Num expect(csv2[9][3]).to eq(plate2_first_oligo) # Index_Barcode_Sequence expect(csv2[9][4]).to eq('2') # Barcode_Plate_Num + + # Third CSV + expect(csv3[9][2]).to eq('Z0193') # Index_Barcode_Num + expect(csv3[9][3]).to eq(plate3_first_oligo) # Index_Barcode_Sequence + expect(csv3[9][4]).to eq('3') # Barcode_Plate_Num + + # Fourth CSV + expect(csv4[9][2]).to eq('Z0289') # Index_Barcode_Num + expect(csv4[9][3]).to eq(plate4_first_oligo) # Index_Barcode_Sequence + expect(csv4[9][4]).to eq('4') # Barcode_Plate_Num end end end From af8d255fd3f2a6681e26ce52419602a115fc949a Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Tue, 24 Mar 2026 13:16:35 +0000 Subject: [PATCH 02/44] added wafer size to ultima requests and pipeline --- app/models/bulk_submission.rb | 6 ++-- app/models/ultima_sequencing_pipeline.rb | 9 ++++++ app/models/ultima_sequencing_request.rb | 5 ++- app/validators/ultima_validator.rb | 10 +++++- app/views/bulk_submissions/_guidance.html.erb | 1 + config/bulk_submission_excel/columns.yml | 19 +++++++++++ config/bulk_submission_excel/ranges.yml | 5 +++ .../003_ultima_request_information_types.yml | 7 ++++ config/locales/metadata/en.yml | 3 ++ ...2536_add_wafer_size_to_request_metadata.rb | 10 ++++++ db/schema.rb | 3 +- spec/data/bulk_submission_excel/columns.yml | 19 +++++++++++ spec/data/bulk_submission_excel/ranges.yml | 5 +++ spec/factories/request_factories.rb | 3 +- .../models/ultima_sequencing_pipeline_spec.rb | 29 +++++++++++++++++ spec/models/ultima_sequencing_request_spec.rb | 8 +++++ spec/validators/ultima_validator_spec.rb | 32 +++++++++++++++++++ 17 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb diff --git a/app/models/bulk_submission.rb b/app/models/bulk_submission.rb index 170ca24bc5..0f29773072 100644 --- a/app/models/bulk_submission.rb +++ b/app/models/bulk_submission.rb @@ -253,7 +253,8 @@ def process # rubocop:todo Metrics/CyclomaticComplexity 'scrna core cells per chip well', '% phix requested', 'low diversity', - 'ot recipe' + 'ot recipe', + 'wafer size' ].freeze ALIAS_FIELDS = { 'plate barcode' => 'barcode', 'tube barcode' => 'barcode' }.freeze @@ -360,7 +361,8 @@ def extract_request_options(details) ['scrna core cells per chip well', 'cells_per_chip_well'], ['% phix requested', 'percent_phix_requested'], ['low diversity', 'low_diversity'], - ['ot recipe', 'ot_recipe'] + ['ot recipe', 'ot_recipe'], + ['wafer size', 'wafer_size'] ].each do |source_key, target_key| assign_value_if_source_present(details, source_key, request_options, target_key) end diff --git a/app/models/ultima_sequencing_pipeline.rb b/app/models/ultima_sequencing_pipeline.rb index 3a28998d3b..7b0b90e847 100644 --- a/app/models/ultima_sequencing_pipeline.rb +++ b/app/models/ultima_sequencing_pipeline.rb @@ -11,6 +11,15 @@ def ot_recipe_consistent_for_batch?(batch) (ot_recipe_list.uniq.size == 1) end + def wafer_size_consistent_for_batch?(batch) + wafer_size_list = batch.requests.filter_map { |request| request.request_metadata.wafer_size } + + # There are some requests that don't have the wafer_size attribute + return false if wafer_size_list.size != batch.requests.size + + (wafer_size_list.uniq.size == 1) + end + def post_release_batch(batch, _user) # Same logic as the superclass, but with a different Messenger root and template batch.assets.compact.uniq.each(&:index_aliquots) diff --git a/app/models/ultima_sequencing_request.rb b/app/models/ultima_sequencing_request.rb index 63be2fae7b..11026d6389 100644 --- a/app/models/ultima_sequencing_request.rb +++ b/app/models/ultima_sequencing_request.rb @@ -6,6 +6,7 @@ class UltimaSequencingRequest < SequencingRequest FREE = 'Free' FLEX = 'Flex' OT_RECIPE_OPTIONS = [FREE, FLEX].freeze + WAFER_SIZE_OPTIONS = %w[5TB 10TB 20TB].freeze has_metadata as: Request do # Defining the sequencing request metadata here again, as 'has_metadata' @@ -25,11 +26,13 @@ class UltimaSequencingRequest < SequencingRequest custom_attribute(:ot_recipe, default: FREE, in: OT_RECIPE_OPTIONS, required: true) enum :ot_recipe, { Free: 0, Flex: 1 } + custom_attribute(:wafer_size, default: '10TB', in: WAFER_SIZE_OPTIONS, required: true) + enum :wafer_size, { '5TB': 0, '10TB': 1, '20TB': 2 } end # Delegate to request_metadata so the attributes are visible to the validator in the RSpec tests. # This delegation has no real effect outside of the tests. - delegate :ot_recipe, to: :request_metadata + delegate :ot_recipe, :wafer_size, to: :request_metadata # Generates unique wafer ID, concatenation of batch_for_opentrons, # id_pool_lims, and request_order. diff --git a/app/validators/ultima_validator.rb b/app/validators/ultima_validator.rb index bc69826792..302e4a5829 100644 --- a/app/validators/ultima_validator.rb +++ b/app/validators/ultima_validator.rb @@ -2,16 +2,18 @@ class UltimaValidator < ActiveModel::Validator TWO_REQUESTS_MSG = 'Batches must contain exactly two requests.' OT_RECIPE_CONSISTENT_MSG = 'OT Recipe must be the same for both requests.' + WAFER_SIZE_CONSISTENT_MSG = 'Wafer size must be the same for both requests.' # Used in _pipeline_limit.html to display custom validation warnings def self.validation_info - 'OT Recipe must be the same for both requests.' + 'OT Recipe and Wafer Size must be the same for both requests.' end # Validates that a batch contains the two requests. def validate(record) validate_exactly_two_requests(record) requests_have_same_ot_recipe(record) + requests_have_same_wafer_size(record) end private @@ -27,4 +29,10 @@ def requests_have_same_ot_recipe(record) record.errors.add(:base, OT_RECIPE_CONSISTENT_MSG) end + + def requests_have_same_wafer_size(record) + return if record.pipeline.wafer_size_consistent_for_batch?(record) + + record.errors.add(:base, WAFER_SIZE_CONSISTENT_MSG) + end end diff --git a/app/views/bulk_submissions/_guidance.html.erb b/app/views/bulk_submissions/_guidance.html.erb index 6532b2d383..4706dfc832 100644 --- a/app/views/bulk_submissions/_guidance.html.erb +++ b/app/views/bulk_submissions/_guidance.html.erb @@ -47,4 +47,5 @@ used to split a submission up into orders, and as a result must have the same re
% PhiX requested
Required percentage of PhiX needed.
Low Diversity
Low Diversity value being "Yes" or "No"
OT Recipe
OT (OpenTron liquid handler) Recipe value being "Free" or "Flex"
+
Wafer Size
Wafer size value being "5TB", "10TB" or "20TB"
diff --git a/config/bulk_submission_excel/columns.yml b/config/bulk_submission_excel/columns.yml index 02de62cb55..8e265391cb 100644 --- a/config/bulk_submission_excel/columns.yml +++ b/config/bulk_submission_excel/columns.yml @@ -367,3 +367,22 @@ ot_recipe: range_name: :ot_recipe conditional_formattings: empty_cell: +wafer_size: + heading: "Wafer Size" + unlocked: true + attribute: :wafer_size + validation: + options: + type: :list + formula1: "$A$1:$A$2" + allowBlank: false + showInputMessage: true + showErrorMessage: true + errorStyle: :stop + errorTitle: "Wafer Size" + error: "You must enter a value from the list provided." + promptTitle: "Wafer Size" + prompt: "Select a value type from the approved list" + range_name: :wafer_size + conditional_formattings: + empty_cell: diff --git a/config/bulk_submission_excel/ranges.yml b/config/bulk_submission_excel/ranges.yml index a1c091015a..b211a116cb 100644 --- a/config/bulk_submission_excel/ranges.yml +++ b/config/bulk_submission_excel/ranges.yml @@ -27,3 +27,8 @@ ot_recipe: options: - "Free" - "Flex" +wafer_size: + options: + - "5TB" + - "10TB" + - "20TB" diff --git a/config/default_records/request_information_types/003_ultima_request_information_types.yml b/config/default_records/request_information_types/003_ultima_request_information_types.yml index 39cabd99c3..6fa20cb84e 100644 --- a/config/default_records/request_information_types/003_ultima_request_information_types.yml +++ b/config/default_records/request_information_types/003_ultima_request_information_types.yml @@ -6,6 +6,13 @@ ot_recipe: width: 5 hide_in_inbox: 0 +wafer_size: + name: Wafer size + key: wafer_size + label: Wafer size + width: 5 + hide_in_inbox: 0 + fragment_size_required_from: name: Fragment size required (from) key: fragment_size_required_from diff --git a/config/locales/metadata/en.yml b/config/locales/metadata/en.yml index 08f1fbd67e..1cee050a2e 100644 --- a/config/locales/metadata/en.yml +++ b/config/locales/metadata/en.yml @@ -79,6 +79,9 @@ en: ot_recipe: label: OT Recipe + wafer_size: + label: Wafer Size + library_creation_request: <<: *REQUEST sequencing_request: diff --git a/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb new file mode 100644 index 0000000000..60f9adc656 --- /dev/null +++ b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# This migration adds a wafer_size column to the request_metadata table, which is used to store +# the wafer size for Ultima sequencing requests. This is stored as an integer enum, with possible +# values of 5TB, 10TB, and 20TB at time of writing. +class AddWaferSizeToRequestMetadata < ActiveRecord::Migration[7.1] + def change + add_column :request_metadata, :wafer_size, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 34cce1cefe..44a678a359 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_03_17_142326) do +ActiveRecord::Schema[7.2].define(version: 2026_03_24_112536) do create_table "accession_sample_statuses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.integer "sample_id", null: false t.string "status", null: false @@ -1191,6 +1191,7 @@ t.boolean "low_diversity" t.integer "percent_phix_requested" t.integer "ot_recipe" + t.integer "wafer_size" t.index ["request_id"], name: "index_request_metadata_on_request_id" end diff --git a/spec/data/bulk_submission_excel/columns.yml b/spec/data/bulk_submission_excel/columns.yml index 03280783ee..ab5bd8739e 100644 --- a/spec/data/bulk_submission_excel/columns.yml +++ b/spec/data/bulk_submission_excel/columns.yml @@ -315,3 +315,22 @@ ot_recipe: range_name: :ot_recipe conditional_formattings: empty_cell: +wafer_size: + heading: "Wafer Size" + unlocked: true + attribute: :wafer_size + validation: + options: + type: :list + formula1: "$A$1:$A$2" + allowBlank: false + showInputMessage: true + showErrorMessage: true + errorStyle: :stop + errorTitle: "Wafer Size" + error: "You must enter a value from the list provided." + promptTitle: "Wafer Size" + prompt: "Select a value type from the approved list" + range_name: :wafer_size + conditional_formattings: + empty_cell: diff --git a/spec/data/bulk_submission_excel/ranges.yml b/spec/data/bulk_submission_excel/ranges.yml index a1c091015a..b211a116cb 100644 --- a/spec/data/bulk_submission_excel/ranges.yml +++ b/spec/data/bulk_submission_excel/ranges.yml @@ -27,3 +27,8 @@ ot_recipe: options: - "Free" - "Flex" +wafer_size: + options: + - "5TB" + - "10TB" + - "20TB" diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb index 5a012985fc..570208fd28 100644 --- a/spec/factories/request_factories.rb +++ b/spec/factories/request_factories.rb @@ -109,7 +109,8 @@ { fragment_size_required_from: 150, fragment_size_required_to: 400, - ot_recipe: 'Free' + ot_recipe: 'Free', + wafer_size: '10TB' } end diff --git a/spec/models/ultima_sequencing_pipeline_spec.rb b/spec/models/ultima_sequencing_pipeline_spec.rb index 9e55c1018e..99cd6dca05 100644 --- a/spec/models/ultima_sequencing_pipeline_spec.rb +++ b/spec/models/ultima_sequencing_pipeline_spec.rb @@ -38,6 +38,35 @@ end end + describe '#wafer_size_consistent_for_batch?' do + it 'returns true when all requests have the same wafer_size' do + batch = pipeline.batches.build + r1 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + r2 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + batch.requests << [r1, r2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be true + end + + it 'returns false when requests have different wafer_sizes' do + batch = pipeline.batches.build + req1 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '5TB' }) + req2 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + batch.requests << [req1, req2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false + end + + it 'returns false when some requests are missing wafer_size' do + batch = pipeline.batches.build + r1 = create(:sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + r2 = create(:sequencing_request, request_metadata_attributes: {}) # no wafer_size + batch.requests << [r1, r2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false + end + end + describe '#post_release_batch' do let(:batch) { create(:batch) } diff --git a/spec/models/ultima_sequencing_request_spec.rb b/spec/models/ultima_sequencing_request_spec.rb index d152679b4e..b990cbb89d 100644 --- a/spec/models/ultima_sequencing_request_spec.rb +++ b/spec/models/ultima_sequencing_request_spec.rb @@ -39,5 +39,13 @@ expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") end end + + context 'when wafer_size value is not assigned' do + it 'is invalid and displays required wafer size error message' do + request.request_metadata.wafer_size = nil + request.validate + expect(request.errors[:'request_metadata.wafer_size']).to include("can't be blank") + end + end end end diff --git a/spec/validators/ultima_validator_spec.rb b/spec/validators/ultima_validator_spec.rb index c651ef2695..1c1ad66aa5 100644 --- a/spec/validators/ultima_validator_spec.rb +++ b/spec/validators/ultima_validator_spec.rb @@ -37,6 +37,38 @@ end end + context 'when batch contains two requests with the same Wafer Size' do + let(:pipeline) { UltimaSequencingPipeline.new } + let(:batch) { create(:batch, pipeline:) } + let(:request1) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) } + let(:request2) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) } + + before do + batch.requests << [request1, request2] + end + + it 'is valid' do + validator.validate(batch) + expect(batch.errors[:base]).to be_empty + end + end + + context 'when batch contains two requests with different wafer_size' do + let(:pipeline) { UltimaSequencingPipeline.new } + let(:batch) { create(:batch, pipeline:) } + let(:request1) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '5TB' }) } + let(:request2) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) } + + before do + batch.requests << [request1, request2] + end + + it 'is invalid due to wafer_size mismatch' do + validator.validate(batch) + expect(batch.errors[:base]).to include(described_class::WAFER_SIZE_CONSISTENT_MSG) + end + end + context 'when batch contains a single request' do let(:pipeline) { UltimaSequencingPipeline.new } let(:batch) { create(:batch, pipeline:) } From ede2885d58298cb3d8ddf8f8b86e40587ebfb499 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 10:01:02 +0000 Subject: [PATCH 03/44] adding ug200 request type --- app/models/ultima_sequencing_request.rb | 5 +- .../ultima_u_g_200_sequencing_request.rb | 61 +++++++++++++++++++ .../003_ultima_request_type_validators.yml | 15 +++++ .../026_ultima_ug200_request_types.yml | 11 ++++ .../021_ultima_ug200_submission_templates.yml | 10 +++ ...2536_add_wafer_size_to_request_metadata.rb | 4 +- db/schema.rb | 3 +- spec/factories/request_factories.rb | 17 +++++- spec/factories/request_type_factories.rb | 5 ++ .../ultima_ug200_sequencing_request_spec.rb | 51 ++++++++++++++++ 10 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 app/models/ultima_u_g_200_sequencing_request.rb create mode 100644 config/default_records/request_type_validators/003_ultima_request_type_validators.yml create mode 100644 config/default_records/request_types/026_ultima_ug200_request_types.yml create mode 100644 config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml create mode 100644 spec/models/ultima_ug200_sequencing_request_spec.rb diff --git a/app/models/ultima_sequencing_request.rb b/app/models/ultima_sequencing_request.rb index 11026d6389..63be2fae7b 100644 --- a/app/models/ultima_sequencing_request.rb +++ b/app/models/ultima_sequencing_request.rb @@ -6,7 +6,6 @@ class UltimaSequencingRequest < SequencingRequest FREE = 'Free' FLEX = 'Flex' OT_RECIPE_OPTIONS = [FREE, FLEX].freeze - WAFER_SIZE_OPTIONS = %w[5TB 10TB 20TB].freeze has_metadata as: Request do # Defining the sequencing request metadata here again, as 'has_metadata' @@ -26,13 +25,11 @@ class UltimaSequencingRequest < SequencingRequest custom_attribute(:ot_recipe, default: FREE, in: OT_RECIPE_OPTIONS, required: true) enum :ot_recipe, { Free: 0, Flex: 1 } - custom_attribute(:wafer_size, default: '10TB', in: WAFER_SIZE_OPTIONS, required: true) - enum :wafer_size, { '5TB': 0, '10TB': 1, '20TB': 2 } end # Delegate to request_metadata so the attributes are visible to the validator in the RSpec tests. # This delegation has no real effect outside of the tests. - delegate :ot_recipe, :wafer_size, to: :request_metadata + delegate :ot_recipe, to: :request_metadata # Generates unique wafer ID, concatenation of batch_for_opentrons, # id_pool_lims, and request_order. diff --git a/app/models/ultima_u_g_200_sequencing_request.rb b/app/models/ultima_u_g_200_sequencing_request.rb new file mode 100644 index 0000000000..ad505477a0 --- /dev/null +++ b/app/models/ultima_u_g_200_sequencing_request.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Request class specific to the Ultima UG200 sequencing platform. +# Includes specific validation for wafer combination with read type. +class UltimaUG200SequencingRequest < SequencingRequest + include Api::Messages::UseqWaferIo::LaneExtensions + + WAFER_SIZE_OPTIONS = %w[5TB 10TB 20TB].freeze + + has_metadata as: Request do + # Defining the sequencing request metadata here again, as 'has_metadata' + # does not automatically append these custom attributes to the request. + # + # The has_metadata call dynamically defines an inner Metadata class and + # takes the attributes from the block and adds them to the Metadata class. + # There is an assumption that the inner Metadata class is available in a + # sequencing request class defintion. Calling has_metadata again does not + # inherit the attributes given in the block supplied in the superclass. + # They need to be supplied again for this class for a proper inner Metadata + # class definition. In a future refactoring these attributes can be moved a + # class attribute and subclasses can merge its own attibutes to that. A + # common method can set up the inner Metadata class in the subclasses. + custom_attribute(:fragment_size_required_from, integer: true, minimum: 1) + custom_attribute(:fragment_size_required_to, integer: true, minimum: 1) + + # TODO: the defaults set here do NOT work on the option lists in screens for some reason. + custom_attribute(:wafer_size, default: '10TB', in: WAFER_SIZE_OPTIONS, required: true) + enum :wafer_size, { '5TB': 0, '10TB': 1, '20TB': 2 } + custom_attribute(:read_length, default: 300, integer: true, validator: true, required: true, selection: true) + end + + # Delegate to request_metadata so the attributes are visible to the validator in the RSpec tests. + # This delegation has no real effect outside of the tests. + delegate :wafer_size, :read_length, to: :request_metadata + + class UltimaUG200RequestOptionsValidator < DelegateValidation::Validator + delegate :wafer_size, :read_length, :request_types, to: :target + + validate :validate_read_length_by_wafer_size + + def validate_read_length_by_wafer_size + puts "Wafer Size: #{wafer_size}, Read Length: #{read_length}" + binding.pry + return if wafer_size == '10TB' && read_length.to_i == 300 + + errors.add(:read_length, + 'The user can only select a Read Length of 300 with the 10TB wafer type for Ultima UG200 requests') + end + end + + def self.delegate_validator + UltimaUG200SequencingRequest::UltimaUG200RequestOptionsValidator + end + + # Generates unique wafer ID, concatenation of batch_for_opentrons, + # id_pool_lims, and request_order. + # @return [String] unique wafer ID for LIMS + def id_wafer_lims + "#{batch.id}_#{source_labware.human_barcode}_#{position}" + end +end diff --git a/config/default_records/request_type_validators/003_ultima_request_type_validators.yml b/config/default_records/request_type_validators/003_ultima_request_type_validators.yml new file mode 100644 index 0000000000..8226313081 --- /dev/null +++ b/config/default_records/request_type_validators/003_ultima_request_type_validators.yml @@ -0,0 +1,15 @@ +--- +WaferSizeRequestedUltimaUG200Sequencing: + request_type_key: ultima_u_g_200_sequencing + request_option: wafer_size + valid_options: + - 5TB + - 10TB + - 20TB +ReadLengthRequestedUltimaUG200Sequencing: + request_type_key: ultima_u_g_200_sequencing + request_option: read_length + valid_options: + - 150 + - 200 + - 300 diff --git a/config/default_records/request_types/026_ultima_ug200_request_types.yml b/config/default_records/request_types/026_ultima_ug200_request_types.yml new file mode 100644 index 0000000000..35581520fd --- /dev/null +++ b/config/default_records/request_types/026_ultima_ug200_request_types.yml @@ -0,0 +1,11 @@ +# Request types for Ultima UG200 sequencing platform. +--- +ultima_u_g_200_sequencing: + name: Ultima UG200 sequencing + asset_type: LibraryTube + order: 2 + initial_state: pending + billable: true + product_line_name: Ultima + request_class_name: UltimaUG200SequencingRequest + request_purpose: standard diff --git a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml new file mode 100644 index 0000000000..27b6a6a6b7 --- /dev/null +++ b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml @@ -0,0 +1,10 @@ +# This submission template is associated with the Ultima PCR Free Library preparation pipeline +# and the Ultima UG200 sequencing platform. +--- +Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing: + submission_class_name: "LinearSubmission" + related_records: + request_type_keys: ["limber_ultima_htp_pcr_free", "limber_multiplexing_ultima", "ultima_u_g_200_sequencing"] + order_role: PCR Free + product_line_name: Ultima + product_catalogue_name: GenericNoPCR diff --git a/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb index 60f9adc656..f1e811d8f0 100644 --- a/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb +++ b/db/migrate/20260324112536_add_wafer_size_to_request_metadata.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true # This migration adds a wafer_size column to the request_metadata table, which is used to store -# the wafer size for Ultima sequencing requests. This is stored as an integer enum, with possible +# the wafer size for Ultima sequencing requests. This is stored as a string, with possible # values of 5TB, 10TB, and 20TB at time of writing. class AddWaferSizeToRequestMetadata < ActiveRecord::Migration[7.1] def change - add_column :request_metadata, :wafer_size, :integer + add_column :request_metadata, :wafer_size, :string end end diff --git a/db/schema.rb b/db/schema.rb index 44a678a359..34cce1cefe 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_03_24_112536) do +ActiveRecord::Schema[7.2].define(version: 2026_03_17_142326) do create_table "accession_sample_statuses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.integer "sample_id", null: false t.string "status", null: false @@ -1191,7 +1191,6 @@ t.boolean "low_diversity" t.integer "percent_phix_requested" t.integer "ot_recipe" - t.integer "wafer_size" t.index ["request_id"], name: "index_request_metadata_on_request_id" end diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb index 570208fd28..fcb9e714a8 100644 --- a/spec/factories/request_factories.rb +++ b/spec/factories/request_factories.rb @@ -109,8 +109,7 @@ { fragment_size_required_from: 150, fragment_size_required_to: 400, - ot_recipe: 'Free', - wafer_size: '10TB' + ot_recipe: 'Free' } end @@ -125,6 +124,20 @@ end end + factory(:ultima_u_g_200_sequencing_request) do + request_type factory: %i[ultima_u_g_200_sequencing] + request_purpose { :standard } + sti_type { 'UltimaUG200SequencingRequest' } + request_metadata_attributes do + { + fragment_size_required_from: 150, + fragment_size_required_to: 400, + read_length: 300, + wafer_size: '10TB' + } + end + end + factory(:library_creation_request, parent: :request, class: 'LibraryCreationRequest') do asset factory: %i[sample_tube] request_type factory: %i[library_creation_request_type] diff --git a/spec/factories/request_type_factories.rb b/spec/factories/request_type_factories.rb index 4d18e1eb3d..536c7ad123 100644 --- a/spec/factories/request_type_factories.rb +++ b/spec/factories/request_type_factories.rb @@ -139,6 +139,11 @@ request_class { UltimaSequencingRequest } end + factory :ultima_u_g_200_sequencing do + asset_type { 'LibraryTube' } + request_class { UltimaUG200SequencingRequest } + end + factory :miseq_sequencing_request_type do request_class { MiSeqSequencingRequest } asset_type { 'LibraryTube' } diff --git a/spec/models/ultima_ug200_sequencing_request_spec.rb b/spec/models/ultima_ug200_sequencing_request_spec.rb new file mode 100644 index 0000000000..424e4020cb --- /dev/null +++ b/spec/models/ultima_ug200_sequencing_request_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UltimaUG200SequencingRequest do + let(:request) { create(:ultima_u_g_200_sequencing_request) } + + describe 'Validations' do + context 'when all attributes are valid' do + it 'is valid' do + expect(request).to be_valid + end + end + + context 'when fragment_size_required_from is less than 1' do + it 'is invalid and displays fragment size from error message' do + request.request_metadata.fragment_size_required_from = 0 + request.validate + expect(request.errors[:'request_metadata.fragment_size_required_from']).to include( + 'must be greater than or equal to 1' + ) + end + end + + context 'when fragment_size_required_to is less than 1' do + it 'is invalid and displays fragment size to error message' do + request.request_metadata.fragment_size_required_to = 0 + request.validate + expect(request.errors[:'request_metadata.fragment_size_required_to']).to include( + 'must be greater than or equal to 1' + ) + end + end + + context 'when ot_recipe value is not assigned' do + it 'is invalid and displays required percent phix requested error message' do + request.request_metadata.ot_recipe = nil + request.validate + expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") + end + end + + context 'when wafer_size value is not assigned' do + it 'is invalid and displays required wafer size error message' do + request.request_metadata.wafer_size = nil + request.validate + expect(request.errors[:'request_metadata.wafer_size']).to include("can't be blank") + end + end + end +end From 5ab4f98dc574786e4f83509710db02aeb14320f4 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 10:03:11 +0000 Subject: [PATCH 04/44] change to schema --- db/schema.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index 34cce1cefe..4f5e30ff8a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_03_17_142326) do +ActiveRecord::Schema[7.2].define(version: 2026_03_24_112536) do create_table "accession_sample_statuses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.integer "sample_id", null: false t.string "status", null: false @@ -1191,6 +1191,7 @@ t.boolean "low_diversity" t.integer "percent_phix_requested" t.integer "ot_recipe" + t.string "wafer_size" t.index ["request_id"], name: "index_request_metadata_on_request_id" end From 9b576426369c8c9582b16c43888fa01e2626d666 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 13:04:45 +0000 Subject: [PATCH 05/44] Added inflection for UG200 Various config refactoring --- app/models/ultima_sequencing_pipeline.rb | 11 +-------- .../ultima_ug200_sequencing_pipeline.rb | 13 +++++++++++ ....rb => ultima_ug200_sequencing_request.rb} | 10 +++----- app/validators/ultima_ug200_validator.rb | 23 +++++++++++++++++++ app/validators/ultima_validator.rb | 10 +------- ...200_pipeline_request_information_types.yml | 17 ++++++++++++++ .../pipelines/004_ultima_ug200_pipelines.yml | 17 ++++++++++++++ .../003_ultima_request_information_types.yml | 7 ------ ...ultima_ug200_request_information_types.yml | 7 ++++++ ..._ultima_ug200_request_type_validators.yml} | 4 ++-- .../026_ultima_ug200_request_types.yml | 3 ++- .../021_ultima_ug200_submission_templates.yml | 10 +++++++- config/initializers/inflections.rb | 1 + config/locales/metadata/en.yml | 3 +++ spec/factories/request_factories.rb | 4 ++-- spec/factories/request_type_factories.rb | 2 +- .../ultima_ug200_sequencing_request_spec.rb | 2 +- 17 files changed, 103 insertions(+), 41 deletions(-) create mode 100644 app/models/ultima_ug200_sequencing_pipeline.rb rename app/models/{ultima_u_g_200_sequencing_request.rb => ultima_ug200_sequencing_request.rb} (89%) create mode 100644 app/validators/ultima_ug200_validator.rb create mode 100644 config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml create mode 100644 config/default_records/pipelines/004_ultima_ug200_pipelines.yml create mode 100644 config/default_records/request_information_types/004_ultima_ug200_request_information_types.yml rename config/default_records/request_type_validators/{003_ultima_request_type_validators.yml => 003_ultima_ug200_request_type_validators.yml} (72%) diff --git a/app/models/ultima_sequencing_pipeline.rb b/app/models/ultima_sequencing_pipeline.rb index 7b0b90e847..639c7ee349 100644 --- a/app/models/ultima_sequencing_pipeline.rb +++ b/app/models/ultima_sequencing_pipeline.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Specialized sequencing pipeline for Ultima +# Specialized sequencing pipeline for Ultima UG100 class UltimaSequencingPipeline < SequencingPipeline def ot_recipe_consistent_for_batch?(batch) ot_recipe_list = batch.requests.filter_map { |request| request.request_metadata.ot_recipe } @@ -11,15 +11,6 @@ def ot_recipe_consistent_for_batch?(batch) (ot_recipe_list.uniq.size == 1) end - def wafer_size_consistent_for_batch?(batch) - wafer_size_list = batch.requests.filter_map { |request| request.request_metadata.wafer_size } - - # There are some requests that don't have the wafer_size attribute - return false if wafer_size_list.size != batch.requests.size - - (wafer_size_list.uniq.size == 1) - end - def post_release_batch(batch, _user) # Same logic as the superclass, but with a different Messenger root and template batch.assets.compact.uniq.each(&:index_aliquots) diff --git a/app/models/ultima_ug200_sequencing_pipeline.rb b/app/models/ultima_ug200_sequencing_pipeline.rb new file mode 100644 index 0000000000..896f4e7696 --- /dev/null +++ b/app/models/ultima_ug200_sequencing_pipeline.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Specialized sequencing pipeline for Ultima UG200 +class UltimaUG200SequencingPipeline < UltimaSequencingPipeline + def wafer_size_consistent_for_batch?(batch) + wafer_size_list = batch.requests.filter_map { |request| request.request_metadata.wafer_size } + + # There are some requests that don't have the wafer_size attribute + return false if wafer_size_list.size != batch.requests.size + + (wafer_size_list.uniq.size == 1) + end +end diff --git a/app/models/ultima_u_g_200_sequencing_request.rb b/app/models/ultima_ug200_sequencing_request.rb similarity index 89% rename from app/models/ultima_u_g_200_sequencing_request.rb rename to app/models/ultima_ug200_sequencing_request.rb index ad505477a0..2f7b569f91 100644 --- a/app/models/ultima_u_g_200_sequencing_request.rb +++ b/app/models/ultima_ug200_sequencing_request.rb @@ -5,8 +5,6 @@ class UltimaUG200SequencingRequest < SequencingRequest include Api::Messages::UseqWaferIo::LaneExtensions - WAFER_SIZE_OPTIONS = %w[5TB 10TB 20TB].freeze - has_metadata as: Request do # Defining the sequencing request metadata here again, as 'has_metadata' # does not automatically append these custom attributes to the request. @@ -23,9 +21,9 @@ class UltimaUG200SequencingRequest < SequencingRequest custom_attribute(:fragment_size_required_from, integer: true, minimum: 1) custom_attribute(:fragment_size_required_to, integer: true, minimum: 1) - # TODO: the defaults set here do NOT work on the option lists in screens for some reason. - custom_attribute(:wafer_size, default: '10TB', in: WAFER_SIZE_OPTIONS, required: true) - enum :wafer_size, { '5TB': 0, '10TB': 1, '20TB': 2 } + # TODO: the defaults set here do NOT work on the option lists in the bulk submission screen, + # but do work on the request additional sequencing screen for some reason. + custom_attribute(:wafer_size, default: '10TB', validator: true, required: true, selection: true) custom_attribute(:read_length, default: 300, integer: true, validator: true, required: true, selection: true) end @@ -39,8 +37,6 @@ class UltimaUG200RequestOptionsValidator < DelegateValidation::Validator validate :validate_read_length_by_wafer_size def validate_read_length_by_wafer_size - puts "Wafer Size: #{wafer_size}, Read Length: #{read_length}" - binding.pry return if wafer_size == '10TB' && read_length.to_i == 300 errors.add(:read_length, diff --git a/app/validators/ultima_ug200_validator.rb b/app/validators/ultima_ug200_validator.rb new file mode 100644 index 0000000000..ed2b32eb9d --- /dev/null +++ b/app/validators/ultima_ug200_validator.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +class UltimaUG200Validator < UltimaValidator + WAFER_SIZE_CONSISTENT_MSG = 'Wafer size must be the same for both requests.' + + # Used in _pipeline_limit.html to display custom validation warnings + def self.validation_info + 'Wafer Size must be the same for both requests.' + end + + # Validates that a batch contains the two requests. + def validate(record) + validate_exactly_two_requests(record) + requests_have_same_wafer_size(record) + end + + private + + def requests_have_same_wafer_size(record) + return if record.pipeline.wafer_size_consistent_for_batch?(record) + + record.errors.add(:base, WAFER_SIZE_CONSISTENT_MSG) + end +end diff --git a/app/validators/ultima_validator.rb b/app/validators/ultima_validator.rb index 302e4a5829..bc69826792 100644 --- a/app/validators/ultima_validator.rb +++ b/app/validators/ultima_validator.rb @@ -2,18 +2,16 @@ class UltimaValidator < ActiveModel::Validator TWO_REQUESTS_MSG = 'Batches must contain exactly two requests.' OT_RECIPE_CONSISTENT_MSG = 'OT Recipe must be the same for both requests.' - WAFER_SIZE_CONSISTENT_MSG = 'Wafer size must be the same for both requests.' # Used in _pipeline_limit.html to display custom validation warnings def self.validation_info - 'OT Recipe and Wafer Size must be the same for both requests.' + 'OT Recipe must be the same for both requests.' end # Validates that a batch contains the two requests. def validate(record) validate_exactly_two_requests(record) requests_have_same_ot_recipe(record) - requests_have_same_wafer_size(record) end private @@ -29,10 +27,4 @@ def requests_have_same_ot_recipe(record) record.errors.add(:base, OT_RECIPE_CONSISTENT_MSG) end - - def requests_have_same_wafer_size(record) - return if record.pipeline.wafer_size_consistent_for_batch?(record) - - record.errors.add(:base, WAFER_SIZE_CONSISTENT_MSG) - end end diff --git a/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml new file mode 100644 index 0000000000..ca846cb34c --- /dev/null +++ b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml @@ -0,0 +1,17 @@ +--- +FragmentSizeRequiredFromInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: fragment_size_required_from + +FragmentSizeRequiredToInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: fragment_size_required_to + +WaferSizeInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: wafer_size + +# TODO: does readlength need to be here +ReadLengthInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 + request_information_type_key: read_length diff --git a/config/default_records/pipelines/004_ultima_ug200_pipelines.yml b/config/default_records/pipelines/004_ultima_ug200_pipelines.yml new file mode 100644 index 0000000000..21b128df05 --- /dev/null +++ b/config/default_records/pipelines/004_ultima_ug200_pipelines.yml @@ -0,0 +1,17 @@ +--- +Ultima UG200: + name: Ultima UG200 Sequencing + sti_type: UltimaUG200SequencingPipeline + validator_class_name: UltimaUG200Validator + sorter: 10 + max_size: 2 + summary: 1 + externally_managed: 0 + group_name: Sequencing + control_request_type_id: 0 + min_size: 1 + request_type_keys: + - ultima_ug200_sequencing + workflow: + name: Ultima UG200 + item_limit: 2 diff --git a/config/default_records/request_information_types/003_ultima_request_information_types.yml b/config/default_records/request_information_types/003_ultima_request_information_types.yml index 6fa20cb84e..39cabd99c3 100644 --- a/config/default_records/request_information_types/003_ultima_request_information_types.yml +++ b/config/default_records/request_information_types/003_ultima_request_information_types.yml @@ -6,13 +6,6 @@ ot_recipe: width: 5 hide_in_inbox: 0 -wafer_size: - name: Wafer size - key: wafer_size - label: Wafer size - width: 5 - hide_in_inbox: 0 - fragment_size_required_from: name: Fragment size required (from) key: fragment_size_required_from diff --git a/config/default_records/request_information_types/004_ultima_ug200_request_information_types.yml b/config/default_records/request_information_types/004_ultima_ug200_request_information_types.yml new file mode 100644 index 0000000000..bde23fdb31 --- /dev/null +++ b/config/default_records/request_information_types/004_ultima_ug200_request_information_types.yml @@ -0,0 +1,7 @@ +--- +wafer_size: + name: Wafer size + key: wafer_size + label: Wafer size + width: 5 + hide_in_inbox: 0 diff --git a/config/default_records/request_type_validators/003_ultima_request_type_validators.yml b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml similarity index 72% rename from config/default_records/request_type_validators/003_ultima_request_type_validators.yml rename to config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml index 8226313081..cf37fb6f84 100644 --- a/config/default_records/request_type_validators/003_ultima_request_type_validators.yml +++ b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml @@ -1,13 +1,13 @@ --- WaferSizeRequestedUltimaUG200Sequencing: - request_type_key: ultima_u_g_200_sequencing + request_type_key: ultima_ug200_sequencing request_option: wafer_size valid_options: - 5TB - 10TB - 20TB ReadLengthRequestedUltimaUG200Sequencing: - request_type_key: ultima_u_g_200_sequencing + request_type_key: ultima_ug200_sequencing request_option: read_length valid_options: - 150 diff --git a/config/default_records/request_types/026_ultima_ug200_request_types.yml b/config/default_records/request_types/026_ultima_ug200_request_types.yml index 35581520fd..99dbf035b3 100644 --- a/config/default_records/request_types/026_ultima_ug200_request_types.yml +++ b/config/default_records/request_types/026_ultima_ug200_request_types.yml @@ -1,11 +1,12 @@ # Request types for Ultima UG200 sequencing platform. --- -ultima_u_g_200_sequencing: +ultima_ug200_sequencing: name: Ultima UG200 sequencing asset_type: LibraryTube order: 2 initial_state: pending billable: true + # Using same product line name as Ultima UG100 product_line_name: Ultima request_class_name: UltimaUG200SequencingRequest request_purpose: standard diff --git a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml index 27b6a6a6b7..cec0787649 100644 --- a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml +++ b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml @@ -4,7 +4,15 @@ Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing: submission_class_name: "LinearSubmission" related_records: - request_type_keys: ["limber_ultima_htp_pcr_free", "limber_multiplexing_ultima", "ultima_u_g_200_sequencing"] + request_type_keys: ["limber_ultima_htp_pcr_free", "limber_multiplexing_ultima", "ultima_ug200_sequencing"] + order_role: PCR Free + product_line_name: Ultima + product_catalogue_name: GenericNoPCR +# TODO: assumption that we need automated version for UG200, but is this the case? +Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing Automated: + submission_class_name: "LinearSubmission" + related_records: + request_type_keys: ["ultima_ug200_sequencing"] order_role: PCR Free product_line_name: Ultima product_catalogue_name: GenericNoPCR diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 55a6a2b9bd..984e1a1a24 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -18,6 +18,7 @@ inflect.acronym 'ENA' # European Nucleotide Archive inflect.acronym 'HTTP' # HyperText Transfer Protocol inflect.acronym 'EBI' # European Bioinformatics Institute + inflect.acronym 'UG200' # Ultima UG200 end # These inflection rules are supported but not enabled by default: diff --git a/config/locales/metadata/en.yml b/config/locales/metadata/en.yml index 1cee050a2e..da8fa0683b 100644 --- a/config/locales/metadata/en.yml +++ b/config/locales/metadata/en.yml @@ -110,6 +110,9 @@ en: ultima_sequencing_request: <<: *REQUEST + ultima_ug200_sequencing_request: + <<: *REQUEST + pulldown: requests: wgs_library_request: diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb index fcb9e714a8..622aa97d85 100644 --- a/spec/factories/request_factories.rb +++ b/spec/factories/request_factories.rb @@ -124,8 +124,8 @@ end end - factory(:ultima_u_g_200_sequencing_request) do - request_type factory: %i[ultima_u_g_200_sequencing] + factory(:ultima_ug200_sequencing_request) do + request_type factory: %i[ultima_ug200_sequencing] request_purpose { :standard } sti_type { 'UltimaUG200SequencingRequest' } request_metadata_attributes do diff --git a/spec/factories/request_type_factories.rb b/spec/factories/request_type_factories.rb index 536c7ad123..87b2fecfd4 100644 --- a/spec/factories/request_type_factories.rb +++ b/spec/factories/request_type_factories.rb @@ -139,7 +139,7 @@ request_class { UltimaSequencingRequest } end - factory :ultima_u_g_200_sequencing do + factory :ultima_ug200_sequencing do asset_type { 'LibraryTube' } request_class { UltimaUG200SequencingRequest } end diff --git a/spec/models/ultima_ug200_sequencing_request_spec.rb b/spec/models/ultima_ug200_sequencing_request_spec.rb index 424e4020cb..d1163868ee 100644 --- a/spec/models/ultima_ug200_sequencing_request_spec.rb +++ b/spec/models/ultima_ug200_sequencing_request_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe UltimaUG200SequencingRequest do - let(:request) { create(:ultima_u_g_200_sequencing_request) } + let(:request) { create(:ultima_ug200_sequencing_request) } describe 'Validations' do context 'when all attributes are valid' do From 2d737b3aff173e3e88d3675f19acae11f7bca4c3 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 14:58:53 +0000 Subject: [PATCH 06/44] added UG200 task descriptors (draft may change) --- .../005_ultima_ug200_descriptors.yml | 85 +++++++++++++++++++ .../tasks/005_ultima_ug200_tasks.yml | 13 +++ 2 files changed, 98 insertions(+) create mode 100644 config/default_records/descriptors/005_ultima_ug200_descriptors.yml create mode 100644 config/default_records/tasks/005_ultima_ug200_tasks.yml diff --git a/config/default_records/descriptors/005_ultima_ug200_descriptors.yml b/config/default_records/descriptors/005_ultima_ug200_descriptors.yml new file mode 100644 index 0000000000..d5c77bcb3e --- /dev/null +++ b/config/default_records/descriptors/005_ultima_ug200_descriptors.yml @@ -0,0 +1,85 @@ +--- +"OTR carrier Lot #": + name: "OTR carrier Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 0 +OTR carrier expiry: + name: OTR carrier expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 1 +"Reaction Mix 7 Lot #": + name: "Reaction Mix 7 Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 2 +Reaction Mix 7 expiry: + name: Reaction Mix 7 expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 3 +"NFW Lot #": + name: "NFW Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 4 +NFW expiry: + name: NFW expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 5 +"Oil Lot #": + name: "Oil Lot #" + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 6 +Oil expiry: + name: Oil expiry + task: Opentrons + workflow: Ultima UG200 + kind: Date + required: false + sorter: 7 +Pipette carousel: + name: Pipette carousel + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: false + sorter: 8 +Opentrons Inst. Name: + name: Opentrons Inst. Name + task: Opentrons + workflow: Ultima UG200 + kind: Text + required: true + sorter: 9 +Assign Control Bead Tube: + name: Assign Control Bead Tube + task: Amp + workflow: Ultima UG200 + kind: Text + required: false + sorter: 0 +UG AMP Inst. Name: + name: UG AMP Inst. Name + task: Amp + workflow: Ultima UG200 + kind: Text + required: true + sorter: 1 diff --git a/config/default_records/tasks/005_ultima_ug200_tasks.yml b/config/default_records/tasks/005_ultima_ug200_tasks.yml new file mode 100644 index 0000000000..c031066648 --- /dev/null +++ b/config/default_records/tasks/005_ultima_ug200_tasks.yml @@ -0,0 +1,13 @@ +--- +Opentrons: + name: Opentrons + workflow: Ultima UG200 + sorted: 0 + lab_activity: true + sti_type: SetDescriptorsTask +Amp: + name: Amp + workflow: Ultima UG200 + sorted: 1 + lab_activity: true + sti_type: SetDescriptorsTask From 4a746a7f82eea621959b1744a670ade19e2895aa Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 15:27:58 +0000 Subject: [PATCH 07/44] fixed tests --- spec/models/ultima_sequencing_request_spec.rb | 8 -------- .../ultima_ug200_sequencing_request_spec.rb | 16 ++++++++-------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/spec/models/ultima_sequencing_request_spec.rb b/spec/models/ultima_sequencing_request_spec.rb index b990cbb89d..d152679b4e 100644 --- a/spec/models/ultima_sequencing_request_spec.rb +++ b/spec/models/ultima_sequencing_request_spec.rb @@ -39,13 +39,5 @@ expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") end end - - context 'when wafer_size value is not assigned' do - it 'is invalid and displays required wafer size error message' do - request.request_metadata.wafer_size = nil - request.validate - expect(request.errors[:'request_metadata.wafer_size']).to include("can't be blank") - end - end end end diff --git a/spec/models/ultima_ug200_sequencing_request_spec.rb b/spec/models/ultima_ug200_sequencing_request_spec.rb index d1163868ee..b0b2696b46 100644 --- a/spec/models/ultima_ug200_sequencing_request_spec.rb +++ b/spec/models/ultima_ug200_sequencing_request_spec.rb @@ -32,14 +32,6 @@ end end - context 'when ot_recipe value is not assigned' do - it 'is invalid and displays required percent phix requested error message' do - request.request_metadata.ot_recipe = nil - request.validate - expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") - end - end - context 'when wafer_size value is not assigned' do it 'is invalid and displays required wafer size error message' do request.request_metadata.wafer_size = nil @@ -47,5 +39,13 @@ expect(request.errors[:'request_metadata.wafer_size']).to include("can't be blank") end end + + context 'when read_length value is not assigned' do + it 'is invalid and displays required read length error message' do + request.request_metadata.read_length = nil + request.validate + expect(request.errors[:'request_metadata.read_length']).to include("can't be blank") + end + end end end From 16e939c4367e114876495c4ef2e8d03cdd5980c1 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Wed, 25 Mar 2026 16:32:16 +0000 Subject: [PATCH 08/44] fixed tests --- .../models/ultima_sequencing_pipeline_spec.rb | 29 ------------ .../ultima_ug200_sequencing_pipeline_spec.rb | 46 +++++++++++++++++++ 2 files changed, 46 insertions(+), 29 deletions(-) create mode 100644 spec/models/ultima_ug200_sequencing_pipeline_spec.rb diff --git a/spec/models/ultima_sequencing_pipeline_spec.rb b/spec/models/ultima_sequencing_pipeline_spec.rb index 99cd6dca05..9e55c1018e 100644 --- a/spec/models/ultima_sequencing_pipeline_spec.rb +++ b/spec/models/ultima_sequencing_pipeline_spec.rb @@ -38,35 +38,6 @@ end end - describe '#wafer_size_consistent_for_batch?' do - it 'returns true when all requests have the same wafer_size' do - batch = pipeline.batches.build - r1 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) - r2 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) - batch.requests << [r1, r2] - - expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be true - end - - it 'returns false when requests have different wafer_sizes' do - batch = pipeline.batches.build - req1 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '5TB' }) - req2 = create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) - batch.requests << [req1, req2] - - expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false - end - - it 'returns false when some requests are missing wafer_size' do - batch = pipeline.batches.build - r1 = create(:sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) - r2 = create(:sequencing_request, request_metadata_attributes: {}) # no wafer_size - batch.requests << [r1, r2] - - expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false - end - end - describe '#post_release_batch' do let(:batch) { create(:batch) } diff --git a/spec/models/ultima_ug200_sequencing_pipeline_spec.rb b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb new file mode 100644 index 0000000000..1f792086cd --- /dev/null +++ b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe UltimaUG200SequencingPipeline do + let(:pipeline) do + described_class.new( + workflow: Workflow.new, + request_types: [create(:ultima_ug200_sequencing)] + ) + end + + describe '#wafer_size_consistent_for_batch?' do + it 'returns true when all requests have the same wafer_size' do + batch = pipeline.batches.build + r1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + r2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + batch.requests << [r1, r2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be true + end + + it 'returns false when requests have different wafer_sizes' do + batch = pipeline.batches.build + req1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '5TB' }) + req2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) + batch.requests << [req1, req2] + + expect(pipeline.wafer_size_consistent_for_batch?(batch)).to be false + end + + # NB. Wafer size is a required field, so cannot be missing. Tested in the request spec. + end + + describe '#post_release_batch' do + let(:batch) { create(:batch) } + + it 'calls Messenger with UseqWaferIo template and useq_wafer root' do + allow(Messenger).to receive(:create!) + pipeline.post_release_batch(batch, create(:user)) + + expect(Messenger).to have_received(:create!).with( + hash_including(target: batch, template: 'UseqWaferIo', root: 'useq_wafer') + ) + end + end +end From a988ce72f10710ad0e69745a6cc9cbefe2c0826a Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 27 Mar 2026 14:37:10 +0000 Subject: [PATCH 09/44] modified ug200 sequencing request to remove read length and add ot recipe --- app/models/ultima_ug200_sequencing_request.rb | 28 +++++------------- ...200_pipeline_request_information_types.yml | 7 ++--- ...3_ultima_ug200_request_type_validators.yml | 7 ----- spec/factories/request_factories.rb | 2 +- .../ultima_ug200_sequencing_pipeline_spec.rb | 29 +++++++++++++++++++ 5 files changed, 41 insertions(+), 32 deletions(-) diff --git a/app/models/ultima_ug200_sequencing_request.rb b/app/models/ultima_ug200_sequencing_request.rb index 2f7b569f91..0587f149dc 100644 --- a/app/models/ultima_ug200_sequencing_request.rb +++ b/app/models/ultima_ug200_sequencing_request.rb @@ -1,10 +1,14 @@ # frozen_string_literal: true # Request class specific to the Ultima UG200 sequencing platform. -# Includes specific validation for wafer combination with read type. +# Includes wafer size and OT recipe. class UltimaUG200SequencingRequest < SequencingRequest include Api::Messages::UseqWaferIo::LaneExtensions + FREE = 'Free' + FLEX = 'Flex' + OT_RECIPE_OPTIONS = [FREE, FLEX].freeze + has_metadata as: Request do # Defining the sequencing request metadata here again, as 'has_metadata' # does not automatically append these custom attributes to the request. @@ -23,30 +27,14 @@ class UltimaUG200SequencingRequest < SequencingRequest # TODO: the defaults set here do NOT work on the option lists in the bulk submission screen, # but do work on the request additional sequencing screen for some reason. + custom_attribute(:ot_recipe, default: FREE, in: OT_RECIPE_OPTIONS, required: true) + enum :ot_recipe, { Free: 0, Flex: 1 } custom_attribute(:wafer_size, default: '10TB', validator: true, required: true, selection: true) - custom_attribute(:read_length, default: 300, integer: true, validator: true, required: true, selection: true) end # Delegate to request_metadata so the attributes are visible to the validator in the RSpec tests. # This delegation has no real effect outside of the tests. - delegate :wafer_size, :read_length, to: :request_metadata - - class UltimaUG200RequestOptionsValidator < DelegateValidation::Validator - delegate :wafer_size, :read_length, :request_types, to: :target - - validate :validate_read_length_by_wafer_size - - def validate_read_length_by_wafer_size - return if wafer_size == '10TB' && read_length.to_i == 300 - - errors.add(:read_length, - 'The user can only select a Read Length of 300 with the 10TB wafer type for Ultima UG200 requests') - end - end - - def self.delegate_validator - UltimaUG200SequencingRequest::UltimaUG200RequestOptionsValidator - end + delegate :wafer_size, :ot_recipe, to: :request_metadata # Generates unique wafer ID, concatenation of batch_for_opentrons, # id_pool_lims, and request_order. diff --git a/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml index ca846cb34c..e39f735c93 100644 --- a/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml +++ b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml @@ -11,7 +11,6 @@ WaferSizeInformationTypeForUltimaUG200: pipeline_name: Ultima UG200 request_information_type_key: wafer_size -# TODO: does readlength need to be here -ReadLengthInformationTypeForUltimaUG200: - pipeline_name: Ultima UG200 - request_information_type_key: read_length +OTRecipeInformationTypeForUltima: + pipeline_name: Ultima + request_information_type_key: ot_recipe diff --git a/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml index cf37fb6f84..028e593253 100644 --- a/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml +++ b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml @@ -6,10 +6,3 @@ WaferSizeRequestedUltimaUG200Sequencing: - 5TB - 10TB - 20TB -ReadLengthRequestedUltimaUG200Sequencing: - request_type_key: ultima_ug200_sequencing - request_option: read_length - valid_options: - - 150 - - 200 - - 300 diff --git a/spec/factories/request_factories.rb b/spec/factories/request_factories.rb index 622aa97d85..ad180e5d76 100644 --- a/spec/factories/request_factories.rb +++ b/spec/factories/request_factories.rb @@ -132,7 +132,7 @@ { fragment_size_required_from: 150, fragment_size_required_to: 400, - read_length: 300, + ot_recipe: 'Free', wafer_size: '10TB' } end diff --git a/spec/models/ultima_ug200_sequencing_pipeline_spec.rb b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb index 1f792086cd..361bce75a5 100644 --- a/spec/models/ultima_ug200_sequencing_pipeline_spec.rb +++ b/spec/models/ultima_ug200_sequencing_pipeline_spec.rb @@ -9,6 +9,35 @@ ) end + describe '#ot_recipe_consistent_for_batch?' do + it 'returns true when all requests have the same ot_recipe' do + batch = pipeline.batches.build + r1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + r2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + batch.requests << [r1, r2] + + expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be true + end + + it 'returns false when requests have different ot_recipes' do + batch = pipeline.batches.build + req1 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + req2 = create(:ultima_ug200_sequencing_request, request_metadata_attributes: { ot_recipe: 'Flex' }) + batch.requests << [req1, req2] + + expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be false + end + + it 'returns false when some requests are missing ot_recipe' do + batch = pipeline.batches.build + r1 = create(:sequencing_request, request_metadata_attributes: { ot_recipe: 'Free' }) + r2 = create(:sequencing_request, request_metadata_attributes: {}) # no ot_recipe + batch.requests << [r1, r2] + + expect(pipeline.ot_recipe_consistent_for_batch?(batch)).to be false + end + end + describe '#wafer_size_consistent_for_batch?' do it 'returns true when all requests have the same wafer_size' do batch = pipeline.batches.build From 72a640df53b92856116cf4408cfc85934a469cb4 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 27 Mar 2026 14:42:09 +0000 Subject: [PATCH 10/44] wip the submission template yml --- ...mplates.yml => 021_ultima_ug200_submission_templates.wip.yml} | 1 - 1 file changed, 1 deletion(-) rename config/default_records/submission_templates/{021_ultima_ug200_submission_templates.yml => 021_ultima_ug200_submission_templates.wip.yml} (89%) diff --git a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml similarity index 89% rename from config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml rename to config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml index cec0787649..e5b5363585 100644 --- a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.yml +++ b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml @@ -8,7 +8,6 @@ Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing: order_role: PCR Free product_line_name: Ultima product_catalogue_name: GenericNoPCR -# TODO: assumption that we need automated version for UG200, but is this the case? Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing Automated: submission_class_name: "LinearSubmission" related_records: From ac9574d42fb0d3b52e2e22feab308f667783d710 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 27 Mar 2026 17:01:50 +0000 Subject: [PATCH 11/44] for ug200 changed readlength for ot recipe --- app/validators/ultima_ug200_validator.rb | 10 +++++++++- spec/models/ultima_ug200_sequencing_request_spec.rb | 8 ++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/validators/ultima_ug200_validator.rb b/app/validators/ultima_ug200_validator.rb index ed2b32eb9d..1dc5c9889e 100644 --- a/app/validators/ultima_ug200_validator.rb +++ b/app/validators/ultima_ug200_validator.rb @@ -1,15 +1,17 @@ # frozen_string_literal: true class UltimaUG200Validator < UltimaValidator WAFER_SIZE_CONSISTENT_MSG = 'Wafer size must be the same for both requests.' + OT_RECIPE_CONSISTENT_MSG = 'OT Recipe must be the same for both requests.' # Used in _pipeline_limit.html to display custom validation warnings def self.validation_info - 'Wafer Size must be the same for both requests.' + 'Wafer Size and OT Recipe must be the same for both requests.' end # Validates that a batch contains the two requests. def validate(record) validate_exactly_two_requests(record) + requests_have_same_ot_recipe(record) requests_have_same_wafer_size(record) end @@ -20,4 +22,10 @@ def requests_have_same_wafer_size(record) record.errors.add(:base, WAFER_SIZE_CONSISTENT_MSG) end + + def requests_have_same_ot_recipe(record) + return if record.pipeline.ot_recipe_consistent_for_batch?(record) + + record.errors.add(:base, OT_RECIPE_CONSISTENT_MSG) + end end diff --git a/spec/models/ultima_ug200_sequencing_request_spec.rb b/spec/models/ultima_ug200_sequencing_request_spec.rb index b0b2696b46..a87bc38fd2 100644 --- a/spec/models/ultima_ug200_sequencing_request_spec.rb +++ b/spec/models/ultima_ug200_sequencing_request_spec.rb @@ -40,11 +40,11 @@ end end - context 'when read_length value is not assigned' do - it 'is invalid and displays required read length error message' do - request.request_metadata.read_length = nil + context 'when ot_recipe value is not assigned' do + it 'is invalid and displays required OT recipe error message' do + request.request_metadata.ot_recipe = nil request.validate - expect(request.errors[:'request_metadata.read_length']).to include("can't be blank") + expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") end end end From 573135fa76790fcebbf04ce93b2382d06ced4633 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 27 Mar 2026 17:05:19 +0000 Subject: [PATCH 12/44] fixed incorrect it text --- spec/models/ultima_sequencing_request_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/ultima_sequencing_request_spec.rb b/spec/models/ultima_sequencing_request_spec.rb index d152679b4e..3cbaa4864f 100644 --- a/spec/models/ultima_sequencing_request_spec.rb +++ b/spec/models/ultima_sequencing_request_spec.rb @@ -33,7 +33,7 @@ end context 'when ot_recipe value is not assigned' do - it 'is invalid and displays required percent phix requested error message' do + it 'is invalid and displays required OT recipe error message' do request.request_metadata.ot_recipe = nil request.validate expect(request.errors[:'request_metadata.ot_recipe']).to include("can't be blank") From 616038139fd6050cccc43ed1e5c34942e1862536 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 12:01:26 +0100 Subject: [PATCH 13/44] refactor(ultima_sample_sheet): convert Generator constants to config methods --- .../sample_sheet_generator.rb | 98 +++++++++++-------- .../sample_sheet_generator_spec.rb | 10 +- 2 files changed, 61 insertions(+), 47 deletions(-) diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index 212b3738c4..ccae90d1de 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -15,33 +15,40 @@ def self.generate(batch) # for each request in the given Ultima sequencing batch. class Generator # rubocop:disable Metrics/ClassLength PLATE_LENGTH = 8 # Assumes 96-well tag plates with 8 rows (A-H). - HEADER_TITLE = ['[Header]'].freeze - GLOBAL_TITLE = ['[Global]'].freeze - GLOBAL_HEADERS = %w[ - Application - sequencing_recipe - analysis_recipe - ].freeze - SAMPLES_TITLE = ['[Samples]'].freeze - SAMPLES_HEADERS = %w[ - Sample_ID - Library_name - Index_Barcode_Num - Index_Barcode_Sequence - Barcode_Plate_Num - Barcode_Plate_Well - application_type - study_id - ].freeze - NUM_COLUMS = SAMPLES_HEADERS.size + def header_title_config + ['[Header]'].freeze + end + + def global_title_config + ['[Global]'].freeze + end + + def global_headers_config + %w[Application sequencing_recipe analysis_recipe].freeze + end + + def samples_title_config + ['[Samples]'].freeze + end + + def samples_headers_config + %w[ + Sample_ID Library_name Index_Barcode_Num Index_Barcode_Sequence + Barcode_Plate_Num Barcode_Plate_Well application_type study_id + ].freeze + end + + def num_colums_config + samples_headers_config.size + end + # The names of the Ultima tag groups are mapped to the index numbers for # the Barcode_Plate_Num column, i.e. 1 or 2. The number is also used for # determining the consistent starting index number for the # Index_Barcode_Num column, i.e. Z0001 or Z097. - ULTIMA_TAG_GROUPS = { - 'Ultima P1' => 1, - 'Ultima P2' => 2 - }.freeze + def ultima_tag_groups_config + { 'Ultima P1' => 1, 'Ultima P2' => 2 }.freeze + end # Initializes the generator with the given batch. # @param batch [UltimaSequencingBatch] the batch to generate sample sheets for @@ -96,7 +103,7 @@ def folder_name # @param csv [CSV] the CSV object to append rows to # @param request [UltimaSequencingRequest] the request whose header data is to be added def add_header_section(csv, request) - csv << pad(HEADER_TITLE) + csv << pad(header_title_config) free_form_text = "Batch #{@batch.id} #{request.asset.human_barcode}" csv << pad([free_form_text]) end @@ -106,8 +113,8 @@ def add_header_section(csv, request) # @param csv [CSV] the CSV object to append rows to # @param _request [UltimaSequencingRequest] the request whose global data is to be added def add_global_section(csv, _request) - csv << pad(GLOBAL_TITLE) - csv << pad(GLOBAL_HEADERS) + csv << pad(global_title_config) + csv << pad(global_headers_config) # Currently there is only one UltimaGlobal record; get the last one. # Future enhancements may allow selecting different records based on # sequencing request or batch properties. @@ -121,22 +128,29 @@ def add_global_section(csv, _request) # @param csv [CSV] the CSV object to append rows to # @param request [UltimaSequencingRequest] the request whose samples are to be added def add_samples_section(csv, request) - csv << pad(SAMPLES_TITLE) - csv << pad(SAMPLES_HEADERS) + csv << pad(samples_title_config) + csv << pad(samples_headers_config) request.asset.aliquots.sort_by(&:id).each do |aliquot| - csv << [ - sample_id_for(aliquot), - library_name_for(aliquot), - index_barcode_num_for(aliquot), - index_barcode_sequence_for(aliquot), - barcode_plate_num_for(aliquot), - barcode_plate_well_for(aliquot), - 'native', # application_type - study_id_for(aliquot) - ] + csv << sample_row_for(aliquot) end end + # Builds a sample section row for a single aliquot. + # @param aliquot [Aliquot] the aliquot whose sample row is needed + # @return [Array] the sample row values + def sample_row_for(aliquot) + [ + sample_id_for(aliquot), + library_name_for(aliquot), + index_barcode_num_for(aliquot), + index_barcode_sequence_for(aliquot), + barcode_plate_num_for(aliquot), + barcode_plate_well_for(aliquot), + 'native', # application_type + study_id_for(aliquot) + ] + end + # Returns a unique sample_ID for the given aliquot. This prefixes numbers # with 's' (lowercase s character) for Sample_ID column, as in s1 … sN, # where N is num of samples on wafer. It starts them with s1 for each wafer, @@ -209,16 +223,16 @@ def tag_index_map end # Returns a mapping of all Ultima tag groups to 1-based index numbers. - # This indexes the tag groups as given in the ULTIMA_TAG_GROUPS hash. + # This indexes the tag groups as given in the ultima_tag_groups_config hash. # @return [Hash{TagGroup => Integer}] mapping of tag groups to index numbers def tag_group_index_map - @tag_group_index_map ||= ultima_tag_groups.index_with { |tg| ULTIMA_TAG_GROUPS[tg.name] } + @tag_group_index_map ||= ultima_tag_groups.index_with { |tg| ultima_tag_groups_config[tg.name] } end # Returns all unique tag groups used for Ultima sequencing from database. # @return [Array] the tag groups used for Ultima sequencing def ultima_tag_groups - @ultima_tag_groups ||= TagGroup.where(name: ULTIMA_TAG_GROUPS.keys) + @ultima_tag_groups ||= TagGroup.where(name: ultima_tag_groups_config.keys) end # Returns the requests associated with the batch. @@ -250,7 +264,7 @@ def sample_id_index_map # @param row [Array] the row to pad (defaults to an empty array) # @return [Array] the padded row def pad(row = []) - row + Array.new(NUM_COLUMS - row.size, '') + row + Array.new(num_colums_config - row.size, '') end end end diff --git a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb index 923a12a490..5d22248184 100644 --- a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb +++ b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb @@ -219,7 +219,7 @@ def map_description(map_id) let(:csv2_samples) { csv_samples_for(request2) } it 'generates header sections' do # rubocop:disable RSpec/MultipleExpectations - expect(csv1[0].compact_blank).to eq(generator.class::HEADER_TITLE) + expect(csv1[0].compact_blank).to eq(generator.header_title_config) expect(csv1[1].compact_blank).to eq(["Batch #{batch.id} #{tube1.human_barcode}"]) # First CSV expect(csv1[2].compact_blank).to eq([]) expect(csv2[1].compact_blank).to eq(["Batch #{batch.id} #{tube2.human_barcode}"]) # Second CSV @@ -228,15 +228,15 @@ def map_description(map_id) it 'generates global sections' do # Test: Add the following hardcoded values, Application(WGS native gDNA), # sequencing_recipe(UG_116cycles_Baseline_1.8.5.2) and analysis_recipe(wgs1) - expect(csv1[3].compact_blank).to eq(generator.class::GLOBAL_TITLE) - expect(csv1[4].compact_blank).to eq(generator.class::GLOBAL_HEADERS) + expect(csv1[3].compact_blank).to eq(generator.global_title_config) + expect(csv1[4].compact_blank).to eq(generator.global_headers_config) expect(csv1[5].compact_blank).to eq(['WGS native gDNA', 'UG_116cycles_Baseline_1.8.5.2', 'wgs1']) expect(csv1[6].compact_blank).to eq([]) end it 'generates samples sections' do - expect(csv1[7].compact_blank).to eq(generator.class::SAMPLES_TITLE) - expect(csv1[8].compact_blank).to eq(generator.class::SAMPLES_HEADERS) + expect(csv1[7].compact_blank).to eq(generator.samples_title_config) + expect(csv1[8].compact_blank).to eq(generator.samples_headers_config) expect(csv1[9..]).to eq(csv1_samples) # First CSV expect(csv2[9..]).to eq(csv2_samples) # Second CSV end From 65ac4e025f1142306a40e98b085f7e77492a3a0c Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 12:14:06 +0100 Subject: [PATCH 14/44] fix(ultima_sample_sheet): sort ultima tag groups by config index order --- app/controllers/ultima_sample_sheet/sample_sheet_generator.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index ccae90d1de..56a2df04c4 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -229,10 +229,12 @@ def tag_group_index_map @tag_group_index_map ||= ultima_tag_groups.index_with { |tg| ultima_tag_groups_config[tg.name] } end - # Returns all unique tag groups used for Ultima sequencing from database. + # Returns all unique tag groups used for Ultima sequencing from database, + # sorted by their index number as defined in ultima_tag_groups_config. # @return [Array] the tag groups used for Ultima sequencing def ultima_tag_groups @ultima_tag_groups ||= TagGroup.where(name: ultima_tag_groups_config.keys) + .sort_by { |tg| ultima_tag_groups_config[tg.name] } end # Returns the requests associated with the batch. From ea30f6277cc3f7bf24a034aa40ab4ee9488d6871 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 13:03:43 +0100 Subject: [PATCH 15/44] fix(ultima_sample_sheet): correct typo in num_columns_config --- app/controllers/ultima_sample_sheet/sample_sheet_generator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index 56a2df04c4..797506a0b5 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -38,7 +38,7 @@ def samples_headers_config ].freeze end - def num_colums_config + def num_columns_config samples_headers_config.size end @@ -266,7 +266,7 @@ def sample_id_index_map # @param row [Array] the row to pad (defaults to an empty array) # @return [Array] the padded row def pad(row = []) - row + Array.new(num_colums_config - row.size, '') + row + Array.new(num_columns_config - row.size, '') end end end From 4daca713d68c14e041d58947c8595ad39f48410a Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 13:25:28 +0100 Subject: [PATCH 16/44] feat(ultima_sample_sheets): add Ultima UG200 sample sheet generator --- .../sample_sheet_generator.rb | 9 ++++- .../ug200_sample_sheet_generator.rb | 34 +++++++++++++++++++ config/initializers/inflections.rb | 1 + 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 app/controllers/ultima_sample_sheet/ug200_sample_sheet_generator.rb diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index 797506a0b5..6344eeb97f 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -15,6 +15,8 @@ def self.generate(batch) # for each request in the given Ultima sequencing batch. class Generator # rubocop:disable Metrics/ClassLength PLATE_LENGTH = 8 # Assumes 96-well tag plates with 8 rows (A-H). + + # Using config methods instead of constants to allow easier overriding. def header_title_config ['[Header]'].freeze end @@ -47,7 +49,12 @@ def num_columns_config # determining the consistent starting index number for the # Index_Barcode_Num column, i.e. Z0001 or Z097. def ultima_tag_groups_config - { 'Ultima P1' => 1, 'Ultima P2' => 2 }.freeze + { + 'Ultima P1' => 1, + 'Ultima P2' => 2, + 'Ultima P3' => 3, + 'UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4)' => 4 + }.freeze end # Initializes the generator with the given batch. diff --git a/app/controllers/ultima_sample_sheet/ug200_sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/ug200_sample_sheet_generator.rb new file mode 100644 index 0000000000..2f3425103b --- /dev/null +++ b/app/controllers/ultima_sample_sheet/ug200_sample_sheet_generator.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module UltimaSampleSheet::UG200SampleSheetGenerator + # Initiates the sample sheet generation for the given batch. + # @param batch [Batch] the Ultima UG200 sequencing batch to generate sample sheets for + # @return [String] the ZIP archive as a binary string + def self.generate(batch) + Generator.new(batch).generate + end + + # Ultima UG200 sample sheet generator class. + # Uses the shared Ultima base implementation with UG200-specific globals and tags. + class Generator < UltimaSampleSheet::SampleSheetGenerator::Generator + def global_headers_config + ['Application'].freeze + end + + # Added ultima_tag_groups_config items to the parent class to make them + # available to all Ultima sample sheet generators. + + private + + # Adds the global section to the CSV for UG200. + # The request parameter is currently unused. + # @param csv [CSV] the CSV object to append rows to + # @param _request [UltimaSequencingRequest] the request whose global data is to be added + def add_global_section(csv, _request) + csv << pad(global_title_config) + csv << pad(global_headers_config) + data = ['WGS Native'] # Application + csv << pad(data) + end + end +end diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 55a6a2b9bd..cd0406ea7e 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -18,6 +18,7 @@ inflect.acronym 'ENA' # European Nucleotide Archive inflect.acronym 'HTTP' # HyperText Transfer Protocol inflect.acronym 'EBI' # European Bioinformatics Institute + inflect.acronym 'UG200' # Ultima Genomics 200 end # These inflection rules are supported but not enabled by default: From defa05d02da8c41014f2ea1af89ccca54e799bcf Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 14:23:58 +0100 Subject: [PATCH 17/44] refactor(ultima_sample_sheets): update tag group config with explicit plate_num/z_start entries --- .../sample_sheet_generator.rb | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index 6344eeb97f..3d9810971f 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -44,16 +44,17 @@ def num_columns_config samples_headers_config.size end - # The names of the Ultima tag groups are mapped to the index numbers for - # the Barcode_Plate_Num column, i.e. 1 or 2. The number is also used for - # determining the consistent starting index number for the - # Index_Barcode_Num column, i.e. Z0001 or Z097. + # Tag group config used to derive CSV plate numbers and Z-index starts. + # Values are explicit to avoid arithmetic assumptions in index mapping. def ultima_tag_groups_config { - 'Ultima P1' => 1, - 'Ultima P2' => 2, - 'Ultima P3' => 3, - 'UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4)' => 4 + 'Ultima P1' => { plate_num: 1, z_start: 1 }, + 'Ultima P2' => { plate_num: 2, z_start: 97 }, + 'Ultima P3' => { plate_num: 3, z_start: 193 }, + 'UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4)' => { + plate_num: 4, + z_start: 289 + } }.freeze end @@ -217,15 +218,14 @@ def study_id_for(aliquot) end # Returns a mapping of all Ultima tags to their respective 1-based index - # numbers. This sorts the tags by their tag group ID and map ID to ensure - # consistent ordering. The index numbers run across all Ultima tag groups, - # i.e. the index is 1 for the first tag in the first tag group and 97 for - # the first tag in the second tag group. + # numbers. Each group's starting index is taken directly from + # ultima_tag_groups_config[:z_start], so numbering is independent of which + # other tag groups exist in the database. # @return [Hash{Tag => Integer}] mapping of tags to index numbers def tag_index_map - @tag_index_map ||= begin - tags = ultima_tag_groups.flat_map { |tg| tg.tags.sort_by(&:map_id) } - tags.each_with_index.to_h { |tag, i| [tag, i + 1] } + @tag_index_map ||= ultima_tag_groups.each_with_object({}) do |tg, map| + start_index = ultima_tag_groups_config[tg.name][:z_start] + tg.tags.sort_by(&:map_id).each_with_index { |tag, i| map[tag] = start_index + i } end end @@ -233,7 +233,7 @@ def tag_index_map # This indexes the tag groups as given in the ultima_tag_groups_config hash. # @return [Hash{TagGroup => Integer}] mapping of tag groups to index numbers def tag_group_index_map - @tag_group_index_map ||= ultima_tag_groups.index_with { |tg| ultima_tag_groups_config[tg.name] } + @tag_group_index_map ||= ultima_tag_groups.index_with { |tg| ultima_tag_groups_config[tg.name][:plate_num] } end # Returns all unique tag groups used for Ultima sequencing from database, @@ -241,7 +241,7 @@ def tag_group_index_map # @return [Array] the tag groups used for Ultima sequencing def ultima_tag_groups @ultima_tag_groups ||= TagGroup.where(name: ultima_tag_groups_config.keys) - .sort_by { |tg| ultima_tag_groups_config[tg.name] } + .sort_by { |tg| ultima_tag_groups_config[tg.name][:plate_num] } end # Returns the requests associated with the batch. From bc5f3d1b69993bd2d3635e8b8a627ab8af161976 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 14:29:58 +0100 Subject: [PATCH 18/44] test(ultima_sample_sheets): add tests for UG200 sample sheet generator --- .../ug200_sample_sheet_generator_spec.rb | 99 +++++++++++++++++++ spec/factories/batch_factories.rb | 4 + spec/factories/pipelines_factories.rb | 12 +++ spec/factories/request_type_factories.rb | 4 + 4 files changed, 119 insertions(+) create mode 100644 spec/controllers/ultima_sample_sheet/ug200_sample_sheet_generator_spec.rb diff --git a/spec/controllers/ultima_sample_sheet/ug200_sample_sheet_generator_spec.rb b/spec/controllers/ultima_sample_sheet/ug200_sample_sheet_generator_spec.rb new file mode 100644 index 0000000000..be36e64150 --- /dev/null +++ b/spec/controllers/ultima_sample_sheet/ug200_sample_sheet_generator_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UltimaSampleSheet::UG200SampleSheetGenerator do + # First oligo sequences for the two UG200 tag groups. + let(:plate3_first_oligo) { 'CTGCACATTGTAGAT' } # Z0193 + let(:plate4_first_oligo) { 'CATCATGCTCCGCTGAT' } # Z0289 + let(:tag_group3_name) { 'Ultima P3' } + let(:tag_group4_name) { 'UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4)' } + + # Eagerly create tag groups and tags to get consistent IDs. + let!(:tag_group3) do + create(:tag_group_with_tags, tag_count: 96, name: tag_group3_name).tap do |tg| + # To test Z0193 matching with the oligo sequence. + tg.tags.first.update!(oligo: plate3_first_oligo) + end + end + + let!(:tag_group4) do + create(:tag_group_with_tags, tag_count: 96, name: tag_group4_name).tap do |tg| + # To test Z0289 matching with the oligo sequence. + tg.tags.first.update!(oligo: plate4_first_oligo) + end + end + let(:tag_groups) { [tag_group3, tag_group4] } + + let(:request_type) { create(:ultima_ug200_sequencing) } + let(:pipeline) { create(:ultima_ug200_sequencing_pipeline, request_types: [request_type]) } + let(:batch) { create(:ultima_sequencing_batch, pipeline:, requests:) } + let(:requests) { [request1, request2] } + let(:request1) { create(:ultima_sequencing_request, asset: tube1.receptacle, request_type: request_type) } + let(:request2) { create(:ultima_sequencing_request, asset: tube2.receptacle, request_type: request_type) } + + # Eagerly create tubes with aliquots to get consistent IDs. + let!(:tube1) do + receptacle = create(:receptacle) + create(:aliquot, tag: tag_group3.tags.first, receptacle: receptacle) + tube = create(:multiplexed_library_tube, receptacle:) + create(:event, content: Time.zone.today.to_s, message: 'scanned in', family: 'scanned_into_lab', eventful: tube) + tube + end + + let!(:tube2) do + receptacle = create(:receptacle) + create(:aliquot, tag: tag_group4.tags.first, receptacle: receptacle) + tube = create(:multiplexed_library_tube, receptacle:) + create(:event, content: Time.zone.today.to_s, message: 'scanned in', family: 'scanned_into_lab', eventful: tube) + tube + end + + # Expected mapping of tag groups to their respective 1-based plate numbers. + let(:tag_group_index_map) { { tag_group3 => 3, tag_group4 => 4 } } + + # Expected mapping of tags to their respective 1-based index numbers. + # Mirrors the generator's tag_index_map: config-value-based offsets. + let(:tag_index_map) do + tag_groups.each_with_object({}) do |tg, map| + start_index = generator.ultima_tag_groups_config[tg.name][:z_start] + tg.tags.sort_by(&:map_id).each_with_index { |tag, i| map[tag] = start_index + i } + end + end + + context 'with csv output' do + subject(:generator) { described_class::Generator.new(batch) } + + # Parse the generated CSV for the tubes into rows and columns. + let(:csv1) { CSV.parse(generator.csv_string(request1), row_sep: "\r\n", nil_value: '') } + let(:csv2) { CSV.parse(generator.csv_string(request2), row_sep: "\r\n", nil_value: '') } + + it 'generates UG200 global section', :aggregate_failures do + expect(generator.global_headers_config).to eq(['Application']) + expect(csv1[3].compact_blank).to eq(generator.global_title_config) + expect(csv1[4].compact_blank).to eq(['Application']) + expect(csv1[5].compact_blank).to eq(['WGS Native']) + end + + it 'uses UG200 tag group mapping config' do + expect(generator.ultima_tag_groups_config.slice(tag_group3_name, tag_group4_name)).to eq( + tag_group3_name => { plate_num: 3, z_start: 193 }, + tag_group4_name => { plate_num: 4, z_start: 289 } + ) + end + + it 'maps the first plate sample indexes and plate number', :aggregate_failures do + expect(csv1[9][2]).to eq('Z0193') + expect(csv1[9][3]).to eq(plate3_first_oligo) + expect(csv1[9][4]).to eq(tag_group_index_map[tag_group3].to_s) + expect(csv1[9][2]).to eq(format('Z%04d', tag_index_map[tag_group3.tags.first])) + end + + it 'maps the second plate sample indexes and plate number', :aggregate_failures do + expect(csv2[9][2]).to eq('Z0289') + expect(csv2[9][3]).to eq(plate4_first_oligo) + expect(csv2[9][4]).to eq(tag_group_index_map[tag_group4].to_s) + expect(csv2[9][2]).to eq(format('Z%04d', tag_index_map[tag_group4.tags.first])) + end + end +end diff --git a/spec/factories/batch_factories.rb b/spec/factories/batch_factories.rb index 6221feffc5..86da9c7ea7 100644 --- a/spec/factories/batch_factories.rb +++ b/spec/factories/batch_factories.rb @@ -40,6 +40,10 @@ pipeline factory: %i[ultima_sequencing_pipeline] end + factory :ultima_ug200_sequencing_batch do + pipeline factory: %i[ultima_ug200_sequencing_pipeline] + end + factory :cherrypick_batch do transient do request_count { 1 } # We create one request by default as cherrypick pipelines have a minimum batch size diff --git a/spec/factories/pipelines_factories.rb b/spec/factories/pipelines_factories.rb index e3b150671b..8cea7dc01f 100644 --- a/spec/factories/pipelines_factories.rb +++ b/spec/factories/pipelines_factories.rb @@ -187,6 +187,18 @@ end end + factory :ultima_ug200_sequencing_pipeline, class: 'Pipeline' do + name { generate(:pipeline_name) } + active { true } + + workflow { build(:lab_workflow_for_pipeline) } + + after(:build) do |pipeline| + pipeline.request_types << create(:ultima_ug200_sequencing) + pipeline.add_control_request_type + end + end + factory :library_completion, class: 'IlluminaHtp::Requests::LibraryCompletion' do request_type do create( diff --git a/spec/factories/request_type_factories.rb b/spec/factories/request_type_factories.rb index 4d18e1eb3d..9d6d087c1a 100644 --- a/spec/factories/request_type_factories.rb +++ b/spec/factories/request_type_factories.rb @@ -139,6 +139,10 @@ request_class { UltimaSequencingRequest } end + # We don't have UltimaUG200SequencingRequest in this PR. + # For now we can use UltimaSequencingRequest as a placeholder. + factory :ultima_ug200_sequencing, parent: :ultima_sequencing + factory :miseq_sequencing_request_type do request_class { MiSeqSequencingRequest } asset_type { 'LibraryTube' } From 4c589434708f46f463f20b6495bc7d477beb309d Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 15:33:35 +0100 Subject: [PATCH 19/44] feat(batches): direct UG200 sample sheet requests to dedicated generator --- app/controllers/batches_controller.rb | 35 ++++++++---- .../ultima_ug200_sequencing_pipeline.rb | 5 ++ spec/controllers/batches_controller_spec.rb | 55 +++++++++++++------ spec/factories/pipelines_factories.rb | 2 +- 4 files changed, 70 insertions(+), 27 deletions(-) create mode 100644 app/models/ultima_ug200_sequencing_pipeline.rb diff --git a/app/controllers/batches_controller.rb b/app/controllers/batches_controller.rb index 717c23fe38..e8afe27aca 100644 --- a/app/controllers/batches_controller.rb +++ b/app/controllers/batches_controller.rb @@ -389,7 +389,10 @@ def find_batch_by_barcode # # @return [Boolean] true if download is allowed, false otherwise def allow_sample_sheet_download? - @batch.pipeline.is_a?(UltimaSequencingPipeline) || logged_in? + return true if @batch.pipeline.is_a?(UltimaUG200SequencingPipeline) + return true if @batch.pipeline.is_a?(UltimaSequencingPipeline) + + logged_in? end # Generates and sends the appropriate sample sheet(s) for the batch. @@ -397,14 +400,12 @@ def allow_sample_sheet_download? def generate_sample_sheet return redirect_to(login_path) unless allow_sample_sheet_download? - if @batch.pipeline.is_a?(ElementAvitiSequencingPipeline) - generate_element_aviti_sample_sheet - elsif @batch.pipeline.is_a?(UltimaSequencingPipeline) - generate_ultima_sample_sheet - else - flash[:error] = 'Sample sheet generation is not supported for this pipeline.' - redirect_to controller: 'batches', action: 'show', id: @batch.id - end + return generate_element_aviti_sample_sheet if @batch.pipeline.is_a?(ElementAvitiSequencingPipeline) + return generate_ultima_ug200_sample_sheet if @batch.pipeline.is_a?(UltimaUG200SequencingPipeline) + return generate_ultima_sample_sheet if @batch.pipeline.is_a?(UltimaSequencingPipeline) + + flash[:error] = 'Sample sheet generation is not supported for this pipeline.' + redirect_to controller: 'batches', action: 'show', id: @batch.id end private @@ -422,7 +423,21 @@ def generate_element_aviti_sample_sheet # Generates and sends the Ultima sample sheet ZIP archive for the batch. # @return [void] def generate_ultima_sample_sheet - zip_string = UltimaSampleSheet::SampleSheetGenerator.generate(@batch) + send_run_manifest_zip(UltimaSampleSheet::SampleSheetGenerator) + end + + # Generates and sends the Ultima UG200 sample sheet ZIP archive for the batch. + # @return [void] + def generate_ultima_ug200_sample_sheet + send_run_manifest_zip(UltimaSampleSheet::UG200SampleSheetGenerator) + end + + # Helper method to generate and send the Ultima sample sheet ZIP archive for + # the batch using the provided generator class. + # @param generator [Class] the sample sheet generator class to use + # @return [void] + def send_run_manifest_zip(generator) + zip_string = generator.generate(@batch) send_data zip_string, type: 'application/zip', filename: "batch_#{@batch.id}_run_manifest.zip", diff --git a/app/models/ultima_ug200_sequencing_pipeline.rb b/app/models/ultima_ug200_sequencing_pipeline.rb new file mode 100644 index 0000000000..fdefa72511 --- /dev/null +++ b/app/models/ultima_ug200_sequencing_pipeline.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Specialized sequencing pipeline for Ultima UG200. +class UltimaUG200SequencingPipeline < UltimaSequencingPipeline +end diff --git a/spec/controllers/batches_controller_spec.rb b/spec/controllers/batches_controller_spec.rb index 9dae4f9a15..56a4a24071 100644 --- a/spec/controllers/batches_controller_spec.rb +++ b/spec/controllers/batches_controller_spec.rb @@ -3,28 +3,52 @@ require 'rails_helper' RSpec.describe BatchesController do + let(:zip_data) { 'FAKE ZIP DATA' } + let(:run_manifest_filename) { "attachment; filename=\"batch_#{batch.id}_run_manifest.zip\"" } + + shared_examples 'returns a run manifest zip file' do + it 'returns a zip file with the correct filename and content', :aggregate_failures do + expect(response.content_type).to eq('application/zip') + expect(response.headers['Content-Disposition']).to include(run_manifest_filename) + expect(response.body).to eq(zip_data) + expect(response).to have_http_status(:ok) + end + end + describe '#generate_ultima_sample_sheet' do let(:current_user) { create(:user) } let(:pipeline) { create(:ultima_sequencing_pipeline) } let(:batch) { create(:batch, pipeline:) } - let(:zip_data) { 'FAKE ZIP DATA' } - - # Expected Content-Disposition header value - let(:content_disposition) do - "attachment; filename=\"batch_#{batch.id}_run_manifest.zip\"" - end before do allow(UltimaSampleSheet::SampleSheetGenerator).to receive(:generate).with(batch).and_return(zip_data) end - shared_examples 'returns a zip file' do - it 'returns a zip file with the correct filename' do # rubocop:disable RSpec/MultipleExpectations - expect(response.content_type).to eq('application/zip') - expect(response.headers['Content-Disposition']).to include(content_disposition) - expect(response.body).to eq(zip_data) - expect(response).to have_http_status(:ok) + context 'when downloading with login' do + before do + get :generate_sample_sheet, params: { id: batch.id }, session: { user: current_user.id } end + + it_behaves_like 'returns a run manifest zip file' + end + + context 'when downloading wihout login' do + # Test: be able to download the file through an url without auth. (Request from NPG team) + before do + get :generate_sample_sheet, params: { id: batch.id } + end + + it_behaves_like 'returns a run manifest zip file' + end + end + + describe '#generate_ultima_ug200_sample_sheet' do + let(:current_user) { create(:user) } + let(:pipeline) { create(:ultima_ug200_sequencing_pipeline) } + let(:batch) { create(:batch, pipeline:) } + + before do + allow(UltimaSampleSheet::UG200SampleSheetGenerator).to receive(:generate).with(batch).and_return(zip_data) end context 'when downloading with login' do @@ -32,16 +56,15 @@ get :generate_sample_sheet, params: { id: batch.id }, session: { user: current_user.id } end - it_behaves_like 'returns a zip file' + it_behaves_like 'returns a run manifest zip file' end - context 'when downloading wihout login' do - # Test: be able to download the file through an url without auth. (Request from NPG team) + context 'when downloading without login' do before do get :generate_sample_sheet, params: { id: batch.id } end - it_behaves_like 'returns a zip file' + it_behaves_like 'returns a run manifest zip file' end end end diff --git a/spec/factories/pipelines_factories.rb b/spec/factories/pipelines_factories.rb index 8cea7dc01f..880f8bcc6a 100644 --- a/spec/factories/pipelines_factories.rb +++ b/spec/factories/pipelines_factories.rb @@ -187,7 +187,7 @@ end end - factory :ultima_ug200_sequencing_pipeline, class: 'Pipeline' do + factory :ultima_ug200_sequencing_pipeline do name { generate(:pipeline_name) } active { true } From 2734596eb9e53b7ffdb1dad61b99c4f1c12b368d Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 15:42:34 +0100 Subject: [PATCH 20/44] test: add ultima_ug200_sequencing factory and request type placeholder for testing --- app/models/ultima_ug200_sequencing_request.rb | 5 +++++ spec/factories/request_type_factories.rb | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 app/models/ultima_ug200_sequencing_request.rb diff --git a/app/models/ultima_ug200_sequencing_request.rb b/app/models/ultima_ug200_sequencing_request.rb new file mode 100644 index 0000000000..a288cd6ed8 --- /dev/null +++ b/app/models/ultima_ug200_sequencing_request.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Request class specific to the Ultima UG200 sequencing platform. +class UltimaUG200SequencingRequest < SequencingRequest +end diff --git a/spec/factories/request_type_factories.rb b/spec/factories/request_type_factories.rb index 9d6d087c1a..87b2fecfd4 100644 --- a/spec/factories/request_type_factories.rb +++ b/spec/factories/request_type_factories.rb @@ -139,9 +139,10 @@ request_class { UltimaSequencingRequest } end - # We don't have UltimaUG200SequencingRequest in this PR. - # For now we can use UltimaSequencingRequest as a placeholder. - factory :ultima_ug200_sequencing, parent: :ultima_sequencing + factory :ultima_ug200_sequencing do + asset_type { 'LibraryTube' } + request_class { UltimaUG200SequencingRequest } + end factory :miseq_sequencing_request_type do request_class { MiSeqSequencingRequest } From 12c6dd6e20c078250a66c375c7c4574ba3f22f8f Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 16:23:16 +0100 Subject: [PATCH 21/44] Revert sample_sheet_generator to develop version --- app/controllers/ultima_sample_sheet/sample_sheet_generator.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index 2fc09674f3..212b3738c4 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -40,9 +40,7 @@ class Generator # rubocop:disable Metrics/ClassLength # Index_Barcode_Num column, i.e. Z0001 or Z097. ULTIMA_TAG_GROUPS = { 'Ultima P1' => 1, - 'Ultima P2' => 2, - 'Ultima P3' => 3, - 'Ultima P4' => 4 + 'Ultima P2' => 2 }.freeze # Initializes the generator with the given batch. From 7f86463d06d55d485d9d5b658c743ef1bb1be0d9 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 16:24:46 +0100 Subject: [PATCH 22/44] Revert sample_sheet_generator spec to develop version --- .../sample_sheet_generator_spec.rb | 81 ++----------------- 1 file changed, 6 insertions(+), 75 deletions(-) diff --git a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb index 419fdfc3f7..923a12a490 100644 --- a/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb +++ b/spec/controllers/ultima_sample_sheet/sample_sheet_generator_spec.rb @@ -53,8 +53,6 @@ # First oligo sequences for the two tag groups. let(:plate1_first_oligo) { 'CAGCTCGAATGCGAT' } let(:plate2_first_oligo) { 'CAGTCAGTTGCAGAT' } - let(:plate3_first_oligo) { 'CTGCACATTGTAGAT' } - let(:plate4_first_oligo) { 'CATCATGCTCCGCTGAT' } # Eagerly create tag groups and tags to get consistent IDs. let!(:tag_group1) do @@ -69,28 +67,14 @@ tg.tags.first.update!(oligo: plate2_first_oligo) end end - let!(:tag_group3) do - create(:tag_group_with_tags, tag_count: 96, name: 'Ultima P3').tap do |tg| - # To test Z0193 matching with the oligo sequence. - tg.tags.first.update!(oligo: plate3_first_oligo) - end - end - let!(:tag_group4) do - create(:tag_group_with_tags, tag_count: 96, name: 'Ultima P4').tap do |tg| - # To test Z0289 matching with the oligo sequence. - tg.tags.first.update!(oligo: plate4_first_oligo) - end - end - let(:tag_groups) { [tag_group1, tag_group2, tag_group3, tag_group4] } + let(:tag_groups) { [tag_group1, tag_group2] } let(:request_type) { create(:ultima_sequencing) } let(:pipeline) { create(:ultima_sequencing_pipeline, request_types: [request_type]) } let(:batch) { create(:ultima_sequencing_batch, pipeline:, requests:) } - let(:requests) { [request1, request2, request3, request4] } + let(:requests) { [request1, request2] } let(:request1) { create(:ultima_sequencing_request, asset: tube1.receptacle, request_type: request_type) } let(:request2) { create(:ultima_sequencing_request, asset: tube2.receptacle, request_type: request_type) } - let(:request3) { create(:ultima_sequencing_request, asset: tube3.receptacle, request_type: request_type) } - let(:request4) { create(:ultima_sequencing_request, asset: tube4.receptacle, request_type: request_type) } # Eagerly create tubes with aliquots to get consistent IDs. let!(:tube1) do @@ -107,23 +91,9 @@ create(:event, content: Time.zone.today.to_s, message: 'scanned in', family: 'scanned_into_lab', eventful: tube) tube end - let!(:tube3) do - receptacle = create(:receptacle) - tag_group3.tags.first(3).map { |tag| create(:aliquot, tag:, receptacle:) } - tube = create(:multiplexed_library_tube, receptacle:) - create(:event, content: Time.zone.today.to_s, message: 'scanned in', family: 'scanned_into_lab', eventful: tube) - tube - end - let!(:tube4) do - receptacle = create(:receptacle) - tag_group4.tags.first(3).map { |tag| create(:aliquot, tag:, receptacle:) } - tube = create(:multiplexed_library_tube, receptacle:) - create(:event, content: Time.zone.today.to_s, message: 'scanned in', family: 'scanned_into_lab', eventful: tube) - tube - end # Expected mapping of tag groups to their respective 1-based plate numbers. - let(:tag_group_index_map) { { tag_group1 => 1, tag_group2 => 2, tag_group3 => 3, tag_group4 => 4 } } + let(:tag_group_index_map) { { tag_group1 => 1, tag_group2 => 2 } } # Expected mapping of tags to their respective 1-based index numbers. let(:tag_index_map) do @@ -199,16 +169,6 @@ def map_description(map_id) csv = "#{request2.id_wafer_lims}.csv" "#{folder}/#{csv}" end - let(:zip_entry3_name) do - folder = "batch_#{batch.id}_sample_sheets" - csv = "#{request3.id_wafer_lims}.csv" - "#{folder}/#{csv}" - end - let(:zip_entry4_name) do - folder = "batch_#{batch.id}_sample_sheets" - csv = "#{request4.id_wafer_lims}.csv" - "#{folder}/#{csv}" - end # Expected CSV section headers from Zip; to peek at the content. let(:zip_content1_header) do "[Header],,,,,,,\r\nBatch #{batch.id} #{tube1.human_barcode},,,,,,,\r\n" @@ -216,20 +176,12 @@ def map_description(map_id) let(:zip_content2_header) do "[Header],,,,,,,\r\nBatch #{batch.id} #{tube2.human_barcode},,,,,,,\r\n" end - let(:zip_content3_header) do - "[Header],,,,,,,\r\nBatch #{batch.id} #{tube3.human_barcode},,,,,,,\r\n" - end - let(:zip_content4_header) do - "[Header],,,,,,,\r\nBatch #{batch.id} #{tube4.human_barcode},,,,,,,\r\n" - end it 'generates valid zip entries' do # Test: The sample manifest (csv file) is generated on user request per pool. # Test: The name should be uniquely identifiable (file name : batchId_NT_number) zip_hash = extract_zip(described_class.generate(batch)) - expect(zip_hash.keys).to contain_exactly( - zip_entry1_name, zip_entry2_name, zip_entry3_name, zip_entry4_name - ) + expect(zip_hash.keys).to contain_exactly(zip_entry1_name, zip_entry2_name) end it 'generates valid zip contents' do @@ -237,9 +189,7 @@ def map_description(map_id) zip_hash = extract_zip(described_class.generate(batch)) expect(zip_hash.values).to contain_exactly( a_string_including(zip_content1_header), - a_string_including(zip_content2_header), - a_string_including(zip_content3_header), - a_string_including(zip_content4_header) + a_string_including(zip_content2_header) ) end @@ -260,26 +210,19 @@ def map_description(map_id) # Parse the generated CSV for the tubes into rows and columns. let(:csv1) { CSV.parse(generator.csv_string(request1), row_sep: "\r\n", nil_value: '') } let(:csv2) { CSV.parse(generator.csv_string(request2), row_sep: "\r\n", nil_value: '') } - let(:csv3) { CSV.parse(generator.csv_string(request3), row_sep: "\r\n", nil_value: '') } - let(:csv4) { CSV.parse(generator.csv_string(request4), row_sep: "\r\n", nil_value: '') } # Test: Adding study_id column to the existing column (study_id per sample) # Expected sample rows let(:csv1_samples) { csv_samples_for(request1) } + let(:csv2_samples) { csv_samples_for(request2) } - let(:csv3_samples) { csv_samples_for(request3) } - let(:csv4_samples) { csv_samples_for(request4) } it 'generates header sections' do # rubocop:disable RSpec/MultipleExpectations expect(csv1[0].compact_blank).to eq(generator.class::HEADER_TITLE) expect(csv1[1].compact_blank).to eq(["Batch #{batch.id} #{tube1.human_barcode}"]) # First CSV expect(csv1[2].compact_blank).to eq([]) expect(csv2[1].compact_blank).to eq(["Batch #{batch.id} #{tube2.human_barcode}"]) # Second CSV - expect(csv2[2].compact_blank).to eq([]) - expect(csv3[1].compact_blank).to eq(["Batch #{batch.id} #{tube3.human_barcode}"]) # Third CSV - expect(csv3[2].compact_blank).to eq([]) - expect(csv4[1].compact_blank).to eq(["Batch #{batch.id} #{tube4.human_barcode}"]) end it 'generates global sections' do @@ -296,8 +239,6 @@ def map_description(map_id) expect(csv1[8].compact_blank).to eq(generator.class::SAMPLES_HEADERS) expect(csv1[9..]).to eq(csv1_samples) # First CSV expect(csv2[9..]).to eq(csv2_samples) # Second CSV - expect(csv3[9..]).to eq(csv3_samples) # Third CSV - expect(csv4[9..]).to eq(csv4_samples) # Fourth CSV end it 'matches the z-indexes, oligo sequences, and plate numbers' do @@ -310,16 +251,6 @@ def map_description(map_id) expect(csv2[9][2]).to eq('Z0097') # Index_Barcode_Num expect(csv2[9][3]).to eq(plate2_first_oligo) # Index_Barcode_Sequence expect(csv2[9][4]).to eq('2') # Barcode_Plate_Num - - # Third CSV - expect(csv3[9][2]).to eq('Z0193') # Index_Barcode_Num - expect(csv3[9][3]).to eq(plate3_first_oligo) # Index_Barcode_Sequence - expect(csv3[9][4]).to eq('3') # Barcode_Plate_Num - - # Fourth CSV - expect(csv4[9][2]).to eq('Z0289') # Index_Barcode_Num - expect(csv4[9][3]).to eq(plate4_first_oligo) # Index_Barcode_Sequence - expect(csv4[9][4]).to eq('4') # Barcode_Plate_Num end end end From 9f5c088b8c59cac89c96ef502354521b66ef4a97 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 16:33:10 +0100 Subject: [PATCH 23/44] chore(ultima): rename for consistent file naming and easy unwip --- .../{006_ultima_p3_and_p4.wip.yml => 006_ultima_ug200.wip.yml} | 0 .../{ultima_p3_and_p4.wip.yml => 002_ultima_ug200.wip.yml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename config/default_records/tag_groups/{006_ultima_p3_and_p4.wip.yml => 006_ultima_ug200.wip.yml} (100%) rename config/default_records/tag_layout_templates/{ultima_p3_and_p4.wip.yml => 002_ultima_ug200.wip.yml} (100%) diff --git a/config/default_records/tag_groups/006_ultima_p3_and_p4.wip.yml b/config/default_records/tag_groups/006_ultima_ug200.wip.yml similarity index 100% rename from config/default_records/tag_groups/006_ultima_p3_and_p4.wip.yml rename to config/default_records/tag_groups/006_ultima_ug200.wip.yml diff --git a/config/default_records/tag_layout_templates/ultima_p3_and_p4.wip.yml b/config/default_records/tag_layout_templates/002_ultima_ug200.wip.yml similarity index 100% rename from config/default_records/tag_layout_templates/ultima_p3_and_p4.wip.yml rename to config/default_records/tag_layout_templates/002_ultima_ug200.wip.yml From 334bb0c596c11afc2e7892f86882341d763b2667 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 16:39:58 +0100 Subject: [PATCH 24/44] feat(ultima_ug200): rename Ultima P4 to UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4) --- config/default_records/tag_groups/006_ultima_ug200.wip.yml | 2 +- .../tag_layout_templates/002_ultima_ug200.wip.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/default_records/tag_groups/006_ultima_ug200.wip.yml b/config/default_records/tag_groups/006_ultima_ug200.wip.yml index cb24166ed9..21af5ef61e 100644 --- a/config/default_records/tag_groups/006_ultima_ug200.wip.yml +++ b/config/default_records/tag_groups/006_ultima_ug200.wip.yml @@ -99,7 +99,7 @@ Ultima P3: 95: CACAGCGCTCCAGAT 96: CACAGAAGATGCGAT -Ultima P4: +UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4): visible: true adapter_type_name: Ultima tags: diff --git a/config/default_records/tag_layout_templates/002_ultima_ug200.wip.yml b/config/default_records/tag_layout_templates/002_ultima_ug200.wip.yml index 72211085a2..71ca967adf 100644 --- a/config/default_records/tag_layout_templates/002_ultima_ug200.wip.yml +++ b/config/default_records/tag_layout_templates/002_ultima_ug200.wip.yml @@ -2,7 +2,7 @@ Ultima P3: :tag_group_name: Ultima P3 :direction_algorithm: TagLayout::InColumns :walking_algorithm: TagLayout::WalkWellsOfPlate -Ultima P4: - :tag_group_name: Ultima P4 +UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4): + :tag_group_name: UG-RD-1916 (Solaris 2.0 V1 PCR-Free Adapters for Ultima Genomics P4) :direction_algorithm: TagLayout::InColumns :walking_algorithm: TagLayout::WalkWellsOfPlate From 260e04dc53846a18c106e4625eef5634076cea75 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 16:43:29 +0100 Subject: [PATCH 25/44] chore(ultima_ug200): change the record loader file name number prefix based on existing files --- .../{002_ultima_ug200.wip.yml => 003_ultima_ug200.wip.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config/default_records/tag_layout_templates/{002_ultima_ug200.wip.yml => 003_ultima_ug200.wip.yml} (100%) diff --git a/config/default_records/tag_layout_templates/002_ultima_ug200.wip.yml b/config/default_records/tag_layout_templates/003_ultima_ug200.wip.yml similarity index 100% rename from config/default_records/tag_layout_templates/002_ultima_ug200.wip.yml rename to config/default_records/tag_layout_templates/003_ultima_ug200.wip.yml From c12e3a23f48737c02244a5a32e5656edfe44f299 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 16:48:11 +0100 Subject: [PATCH 26/44] doc(ultima_ug200): add title to ug200 record loader files --- config/default_records/tag_groups/006_ultima_ug200.wip.yml | 3 +++ .../tag_layout_templates/003_ultima_ug200.wip.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/config/default_records/tag_groups/006_ultima_ug200.wip.yml b/config/default_records/tag_groups/006_ultima_ug200.wip.yml index 21af5ef61e..e76b585f3b 100644 --- a/config/default_records/tag_groups/006_ultima_ug200.wip.yml +++ b/config/default_records/tag_groups/006_ultima_ug200.wip.yml @@ -1,3 +1,6 @@ +--- +# Ultima UG200 tag groups + Ultima P3: visible: true adapter_type_name: Ultima diff --git a/config/default_records/tag_layout_templates/003_ultima_ug200.wip.yml b/config/default_records/tag_layout_templates/003_ultima_ug200.wip.yml index 71ca967adf..e2f1d0fb31 100644 --- a/config/default_records/tag_layout_templates/003_ultima_ug200.wip.yml +++ b/config/default_records/tag_layout_templates/003_ultima_ug200.wip.yml @@ -1,3 +1,6 @@ +--- +# Ultima UG200 tag layout templates + Ultima P3: :tag_group_name: Ultima P3 :direction_algorithm: TagLayout::InColumns From 5dbd4a929eba9cea768464a7e8adb751f8bd4cd4 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 13:23:30 +0100 Subject: [PATCH 27/44] test(validators): split UG200 checks into dedicated ultima_ug200 validator spec --- .../validators/ultima_ug200_validator_spec.rb | 61 +++++++++++++++++++ spec/validators/ultima_validator_spec.rb | 32 ---------- 2 files changed, 61 insertions(+), 32 deletions(-) create mode 100644 spec/validators/ultima_ug200_validator_spec.rb diff --git a/spec/validators/ultima_ug200_validator_spec.rb b/spec/validators/ultima_ug200_validator_spec.rb new file mode 100644 index 0000000000..34082981cb --- /dev/null +++ b/spec/validators/ultima_ug200_validator_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true +require 'rails_helper' + +describe UltimaUG200Validator do + describe '#validate' do + subject(:validator) { described_class.new } + + let(:pipeline) { UltimaUG200SequencingPipeline.new } + let(:batch) { create(:batch, pipeline:) } + let(:requests) { [request1, request2] } + let(:request1_metadata) { { ot_recipe: 'Free', wafer_size: '10TB' } } + let(:request2_metadata) { { ot_recipe: 'Free', wafer_size: '10TB' } } + let(:request1) { create(:ultima_sequencing_request, request_metadata_attributes: request1_metadata) } + let(:request2) { create(:ultima_sequencing_request, request_metadata_attributes: request2_metadata) } + + before do + batch.requests << requests + end + + context 'when batch contains two requests with the same OT Recipe' do + it 'is valid' do + validator.validate(batch) + expect(batch.errors[:base]).to be_empty + end + end + + context 'when batch contains two requests with different ot_recipe' do + let(:request2_metadata) { { ot_recipe: 'Flex', wafer_size: '10TB' } } + + it 'is invalid due to ot_recipe mismatch' do + validator.validate(batch) + expect(batch.errors[:base]).to include(described_class::OT_RECIPE_CONSISTENT_MSG) + end + end + + context 'when batch contains two requests with the same Wafer Size' do + it 'is valid' do + validator.validate(batch) + expect(batch.errors[:base]).not_to include(described_class::WAFER_SIZE_CONSISTENT_MSG) + end + end + + context 'when batch contains two requests with different wafer_size' do + let(:request1_metadata) { { ot_recipe: 'Free', wafer_size: '5TB' } } + + it 'is invalid due to wafer_size mismatch' do + validator.validate(batch) + expect(batch.errors[:base]).to include(described_class::WAFER_SIZE_CONSISTENT_MSG) + end + end + + context 'when batch contains a single request' do + let(:requests) { [request1] } + + it 'is invalid' do + validator.validate(batch) + expect(batch.errors[:base]).to include(described_class::TWO_REQUESTS_MSG) + end + end + end +end diff --git a/spec/validators/ultima_validator_spec.rb b/spec/validators/ultima_validator_spec.rb index 1c1ad66aa5..c651ef2695 100644 --- a/spec/validators/ultima_validator_spec.rb +++ b/spec/validators/ultima_validator_spec.rb @@ -37,38 +37,6 @@ end end - context 'when batch contains two requests with the same Wafer Size' do - let(:pipeline) { UltimaSequencingPipeline.new } - let(:batch) { create(:batch, pipeline:) } - let(:request1) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) } - let(:request2) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) } - - before do - batch.requests << [request1, request2] - end - - it 'is valid' do - validator.validate(batch) - expect(batch.errors[:base]).to be_empty - end - end - - context 'when batch contains two requests with different wafer_size' do - let(:pipeline) { UltimaSequencingPipeline.new } - let(:batch) { create(:batch, pipeline:) } - let(:request1) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '5TB' }) } - let(:request2) { create(:ultima_sequencing_request, request_metadata_attributes: { wafer_size: '10TB' }) } - - before do - batch.requests << [request1, request2] - end - - it 'is invalid due to wafer_size mismatch' do - validator.validate(batch) - expect(batch.errors[:base]).to include(described_class::WAFER_SIZE_CONSISTENT_MSG) - end - end - context 'when batch contains a single request' do let(:pipeline) { UltimaSequencingPipeline.new } let(:batch) { create(:batch, pipeline:) } From e8b64f2f24a36922c2f375a841576e269a814332 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 13:53:43 +0100 Subject: [PATCH 28/44] fix(submissions): apply selection defaults for blank values --- app/helpers/submissions_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/submissions_helper.rb b/app/helpers/submissions_helper.rb index 7e12703c8e..49bcc6e0b8 100644 --- a/app/helpers/submissions_helper.rb +++ b/app/helpers/submissions_helper.rb @@ -139,7 +139,8 @@ def submission_link(submission, options) def field_selection_tag(request_options, field_info, name_format, enforce_required) select_tag( name_format % field_info.key, - options_for_select(field_info.selection.map(&:to_s), request_options[field_info.key]), + options_for_select(field_info.selection.map(&:to_s), + request_options[field_info.key].presence || field_info.default_value), class: 'custom-select', required: enforce_required && field_info.required, read_only: field_info.selection.size == 1 From fcbcd9cefe078c76e6eb4c9c81ab9fa35352ed60 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 13:55:13 +0100 Subject: [PATCH 29/44] docs(remove todo commit about selection defaults) --- app/models/ultima_ug200_sequencing_request.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/ultima_ug200_sequencing_request.rb b/app/models/ultima_ug200_sequencing_request.rb index 0587f149dc..0bed36c602 100644 --- a/app/models/ultima_ug200_sequencing_request.rb +++ b/app/models/ultima_ug200_sequencing_request.rb @@ -25,8 +25,6 @@ class UltimaUG200SequencingRequest < SequencingRequest custom_attribute(:fragment_size_required_from, integer: true, minimum: 1) custom_attribute(:fragment_size_required_to, integer: true, minimum: 1) - # TODO: the defaults set here do NOT work on the option lists in the bulk submission screen, - # but do work on the request additional sequencing screen for some reason. custom_attribute(:ot_recipe, default: FREE, in: OT_RECIPE_OPTIONS, required: true) enum :ot_recipe, { Free: 0, Flex: 1 } custom_attribute(:wafer_size, default: '10TB', validator: true, required: true, selection: true) From bd33b3a680191dfa7007fbbf94d0197a6e6fde31 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 13:55:49 +0100 Subject: [PATCH 30/44] test(submissions): apply selection defaults for blank values --- spec/helpers/submissions_helper_spec.rb | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 spec/helpers/submissions_helper_spec.rb diff --git a/spec/helpers/submissions_helper_spec.rb b/spec/helpers/submissions_helper_spec.rb new file mode 100644 index 0000000000..5e5101c42c --- /dev/null +++ b/spec/helpers/submissions_helper_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe SubmissionsHelper do + describe '#field_input_tag' do + let(:field_info) do + double( + kind: 'Selection', + key: :ot_recipe, + selection: %w[Free Flex], + default_value: 'Free', + required: true + ) + end + + let(:wafer_size_field_info) do + double( + kind: 'Selection', + key: :wafer_size, + selection: %w[5TB 10TB], + default_value: '10TB', + required: true + ) + end + + it 'uses the default value for selection fields when no request option is set' do + html = helper.field_input_tag(field_info, values: {}, name_format: 'submission[order_params][%s]') + + expect(html).to include('') + end + + it 'uses the default value for selection fields when request option is blank' do + html = helper.field_input_tag(field_info, values: { ot_recipe: '' }, name_format: 'submission[order_params][%s]') + + expect(html).to include('') + end + + it 'uses the default wafer_size when request option is blank' do + html = helper.field_input_tag(wafer_size_field_info, + values: { wafer_size: '' }, + name_format: 'submission[order_params][%s]') + + expect(html).to include('') + end + end +end From ac0602bc4d83ccb05b469b5dc300dd2681457443 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 14:03:30 +0100 Subject: [PATCH 31/44] fix(records): correct OTRecipeInformationTypeForUltimaUG200 association --- .../004_ultima_ug200_pipeline_request_information_types.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml index e39f735c93..76a81c2576 100644 --- a/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml +++ b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml @@ -11,6 +11,6 @@ WaferSizeInformationTypeForUltimaUG200: pipeline_name: Ultima UG200 request_information_type_key: wafer_size -OTRecipeInformationTypeForUltima: - pipeline_name: Ultima +OTRecipeInformationTypeForUltimaUG200: + pipeline_name: Ultima UG200 request_information_type_key: ot_recipe From 7a2d52d348acd09c540d6dd4ae9bfde10bff9953 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 16:30:14 +0100 Subject: [PATCH 32/44] chore(records): rename ultima_ug200 files to wip --- ...ug200_descriptors.yml => 005_ultima_ug200_descriptors.wip.yml} | 0 ...> 004_ultima_ug200_pipeline_request_information_types.wip.yml} | 0 ...ima_ug200_pipelines.yml => 004_ultima_ug200_pipelines.wip.yml} | 0 ...pes.yml => 004_ultima_ug200_request_information_types.wip.yml} | 0 ...ators.yml => 003_ultima_ug200_request_type_validators.wip.yml} | 0 ...0_request_types.yml => 026_ultima_ug200_request_types.wip.yml} | 0 ...{005_ultima_ug200_tasks.yml => 005_ultima_ug200_tasks.wip.yml} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename config/default_records/descriptors/{005_ultima_ug200_descriptors.yml => 005_ultima_ug200_descriptors.wip.yml} (100%) rename config/default_records/pipeline_request_information_types/{004_ultima_ug200_pipeline_request_information_types.yml => 004_ultima_ug200_pipeline_request_information_types.wip.yml} (100%) rename config/default_records/pipelines/{004_ultima_ug200_pipelines.yml => 004_ultima_ug200_pipelines.wip.yml} (100%) rename config/default_records/request_information_types/{004_ultima_ug200_request_information_types.yml => 004_ultima_ug200_request_information_types.wip.yml} (100%) rename config/default_records/request_type_validators/{003_ultima_ug200_request_type_validators.yml => 003_ultima_ug200_request_type_validators.wip.yml} (100%) rename config/default_records/request_types/{026_ultima_ug200_request_types.yml => 026_ultima_ug200_request_types.wip.yml} (100%) rename config/default_records/tasks/{005_ultima_ug200_tasks.yml => 005_ultima_ug200_tasks.wip.yml} (100%) diff --git a/config/default_records/descriptors/005_ultima_ug200_descriptors.yml b/config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml similarity index 100% rename from config/default_records/descriptors/005_ultima_ug200_descriptors.yml rename to config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml diff --git a/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml b/config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.wip.yml similarity index 100% rename from config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.yml rename to config/default_records/pipeline_request_information_types/004_ultima_ug200_pipeline_request_information_types.wip.yml diff --git a/config/default_records/pipelines/004_ultima_ug200_pipelines.yml b/config/default_records/pipelines/004_ultima_ug200_pipelines.wip.yml similarity index 100% rename from config/default_records/pipelines/004_ultima_ug200_pipelines.yml rename to config/default_records/pipelines/004_ultima_ug200_pipelines.wip.yml diff --git a/config/default_records/request_information_types/004_ultima_ug200_request_information_types.yml b/config/default_records/request_information_types/004_ultima_ug200_request_information_types.wip.yml similarity index 100% rename from config/default_records/request_information_types/004_ultima_ug200_request_information_types.yml rename to config/default_records/request_information_types/004_ultima_ug200_request_information_types.wip.yml diff --git a/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml b/config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.wip.yml similarity index 100% rename from config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.yml rename to config/default_records/request_type_validators/003_ultima_ug200_request_type_validators.wip.yml diff --git a/config/default_records/request_types/026_ultima_ug200_request_types.yml b/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml similarity index 100% rename from config/default_records/request_types/026_ultima_ug200_request_types.yml rename to config/default_records/request_types/026_ultima_ug200_request_types.wip.yml diff --git a/config/default_records/tasks/005_ultima_ug200_tasks.yml b/config/default_records/tasks/005_ultima_ug200_tasks.wip.yml similarity index 100% rename from config/default_records/tasks/005_ultima_ug200_tasks.yml rename to config/default_records/tasks/005_ultima_ug200_tasks.wip.yml From 9e6b11ef305feab2de1278481679e8ad960259ef Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 17:12:38 +0100 Subject: [PATCH 33/44] fix(records): make task section names unique insider record loader folder --- config/default_records/tasks/005_ultima_ug200_tasks.wip.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/default_records/tasks/005_ultima_ug200_tasks.wip.yml b/config/default_records/tasks/005_ultima_ug200_tasks.wip.yml index c031066648..a72c05029d 100644 --- a/config/default_records/tasks/005_ultima_ug200_tasks.wip.yml +++ b/config/default_records/tasks/005_ultima_ug200_tasks.wip.yml @@ -1,11 +1,12 @@ --- -Opentrons: +# Section names must be unique inside the record loader folder. +Opentrons UG200: name: Opentrons workflow: Ultima UG200 sorted: 0 lab_activity: true sti_type: SetDescriptorsTask -Amp: +Amp UG200: name: Amp workflow: Ultima UG200 sorted: 1 From db7c961e7b675fd3390aebea261b44bb974ebb41 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 17:14:37 +0100 Subject: [PATCH 34/44] fix(records): make descriptor section names unique within the record loader folder --- .../005_ultima_ug200_descriptors.wip.yml | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml b/config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml index d5c77bcb3e..df4f31fa60 100644 --- a/config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml +++ b/config/default_records/descriptors/005_ultima_ug200_descriptors.wip.yml @@ -1,82 +1,83 @@ --- -"OTR carrier Lot #": +# Section names must be unique inside the record loader folder. +"OTR carrier Lot # UG200": name: "OTR carrier Lot #" task: Opentrons workflow: Ultima UG200 kind: Text required: false sorter: 0 -OTR carrier expiry: +OTR carrier expiry UG200: name: OTR carrier expiry task: Opentrons workflow: Ultima UG200 kind: Date required: false sorter: 1 -"Reaction Mix 7 Lot #": +"Reaction Mix 7 Lot # UG200": name: "Reaction Mix 7 Lot #" task: Opentrons workflow: Ultima UG200 kind: Text required: false sorter: 2 -Reaction Mix 7 expiry: +Reaction Mix 7 expiry UG200: name: Reaction Mix 7 expiry task: Opentrons workflow: Ultima UG200 kind: Date required: false sorter: 3 -"NFW Lot #": +"NFW Lot # UG200": name: "NFW Lot #" task: Opentrons workflow: Ultima UG200 kind: Text required: false sorter: 4 -NFW expiry: +NFW expiry UG200: name: NFW expiry task: Opentrons workflow: Ultima UG200 kind: Date required: false sorter: 5 -"Oil Lot #": +"Oil Lot # UG200": name: "Oil Lot #" task: Opentrons workflow: Ultima UG200 kind: Text required: false sorter: 6 -Oil expiry: +Oil expiry UG200: name: Oil expiry task: Opentrons workflow: Ultima UG200 kind: Date required: false sorter: 7 -Pipette carousel: +Pipette carousel UG200: name: Pipette carousel task: Opentrons workflow: Ultima UG200 kind: Text required: false sorter: 8 -Opentrons Inst. Name: +Opentrons Inst. Name UG200: name: Opentrons Inst. Name task: Opentrons workflow: Ultima UG200 kind: Text required: true sorter: 9 -Assign Control Bead Tube: +Assign Control Bead Tube UG200: name: Assign Control Bead Tube task: Amp workflow: Ultima UG200 kind: Text required: false sorter: 0 -UG AMP Inst. Name: +UG AMP Inst. Name UG200: name: UG AMP Inst. Name task: Amp workflow: Ultima UG200 From 42f363d494b7a771818b33ede50cec7002c3b9d4 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 17:49:48 +0100 Subject: [PATCH 35/44] feat(config): add UG200 specific request types because of purpose dependencies --- .../026_ultima_ug200_request_types.wip.yml | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml b/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml index 99dbf035b3..81b0d39c15 100644 --- a/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml +++ b/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml @@ -1,5 +1,25 @@ # Request types for Ultima UG200 sequencing platform. --- +limber_ultima_ug200_htp_pcr_free: + name: Limber Ultima UG200 HTP PCR Free + order: 1 + request_class_name: IlluminaHtp::Requests::StdLibraryRequest + asset_type: Well + for_multiplexing: false + billable: true + product_line_name: Ultima # same as UG100 + acceptable_purposes: + - UPF2 Cherrypicked + library_types: + - Ultima High Throughput PCR Free 96 # same as UG100 +limber_multiplexing_ultima_ug200: + name: Limber Multiplexing Ultima UG200 + asset_type: Well + order: 2 + request_class_name: Request::Multiplexing + for_multiplexing: true + product_line_name: Ultima # same as UG100 + target_purpose_name: UPF2 EqVol Norm ultima_ug200_sequencing: name: Ultima UG200 sequencing asset_type: LibraryTube @@ -7,6 +27,6 @@ ultima_ug200_sequencing: initial_state: pending billable: true # Using same product line name as Ultima UG100 - product_line_name: Ultima + product_line_name: Ultima # same as UG100 request_class_name: UltimaUG200SequencingRequest request_purpose: standard From 9e1df9005506886bf21cf3ad767d24d88b829a38 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 17:51:22 +0100 Subject: [PATCH 36/44] feat(config): update request type keys of the Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing submission template --- .../021_ultima_ug200_submission_templates.wip.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml index e5b5363585..9d9ee62579 100644 --- a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml +++ b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml @@ -4,14 +4,14 @@ Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing: submission_class_name: "LinearSubmission" related_records: - request_type_keys: ["limber_ultima_htp_pcr_free", "limber_multiplexing_ultima", "ultima_ug200_sequencing"] + request_type_keys: ["limber_ultima_ug200_htp_pcr_free", "limber_multiplexing_ultima_ug200", "ultima_ug200_sequencing"] order_role: PCR Free - product_line_name: Ultima + product_line_name: Ultima # same as UG100 product_catalogue_name: GenericNoPCR Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing Automated: submission_class_name: "LinearSubmission" related_records: request_type_keys: ["ultima_ug200_sequencing"] order_role: PCR Free - product_line_name: Ultima + product_line_name: Ultima # same as UG100 product_catalogue_name: GenericNoPCR From 82926a0fb65a18faf4368e3b793dc822acda3c1d Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 17:56:43 +0100 Subject: [PATCH 37/44] style(config): prettier format spacing and indentation --- .../request_types/026_ultima_ug200_request_types.wip.yml | 2 +- .../021_ultima_ug200_submission_templates.wip.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml b/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml index 81b0d39c15..bde1c5fc6e 100644 --- a/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml +++ b/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml @@ -11,7 +11,7 @@ limber_ultima_ug200_htp_pcr_free: acceptable_purposes: - UPF2 Cherrypicked library_types: - - Ultima High Throughput PCR Free 96 # same as UG100 + - Ultima High Throughput PCR Free 96 # same as UG100 limber_multiplexing_ultima_ug200: name: Limber Multiplexing Ultima UG200 asset_type: Well diff --git a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml index 9d9ee62579..29f30ede36 100644 --- a/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml +++ b/config/default_records/submission_templates/021_ultima_ug200_submission_templates.wip.yml @@ -4,7 +4,8 @@ Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing: submission_class_name: "LinearSubmission" related_records: - request_type_keys: ["limber_ultima_ug200_htp_pcr_free", "limber_multiplexing_ultima_ug200", "ultima_ug200_sequencing"] + request_type_keys: + ["limber_ultima_ug200_htp_pcr_free", "limber_multiplexing_ultima_ug200", "ultima_ug200_sequencing"] order_role: PCR Free product_line_name: Ultima # same as UG100 product_catalogue_name: GenericNoPCR From 83e3f306a333005681ed35df3ac12886521139bd Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 17:59:30 +0100 Subject: [PATCH 38/44] feat(config): add UPF2 Cherrypicked purpose required by Limber Ultima UG200 HTP PCR Free request type --- .../plate_purposes/017_ultima_ug200_purposes.wip.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 config/default_records/plate_purposes/017_ultima_ug200_purposes.wip.yml diff --git a/config/default_records/plate_purposes/017_ultima_ug200_purposes.wip.yml b/config/default_records/plate_purposes/017_ultima_ug200_purposes.wip.yml new file mode 100644 index 0000000000..682ce88e23 --- /dev/null +++ b/config/default_records/plate_purposes/017_ultima_ug200_purposes.wip.yml @@ -0,0 +1,9 @@ +--- +# 96 well input plates +UPF2 Cherrypicked: + type: PlatePurpose::Input + stock_plate: true + default_state: passed + cherrypickable_target: true + target_type: Plate + size: 96 From 7c271196917dd9cd5a2430efbaf51ccd90ee5ed8 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 20:24:15 +0100 Subject: [PATCH 39/44] feat(config): add ultima ug200 tube purposes --- .../011_ultima_ug200_tube_purposes.wip.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 config/default_records/tube_purposes/011_ultima_ug200_tube_purposes.wip.yml diff --git a/config/default_records/tube_purposes/011_ultima_ug200_tube_purposes.wip.yml b/config/default_records/tube_purposes/011_ultima_ug200_tube_purposes.wip.yml new file mode 100644 index 0000000000..dd484cbd91 --- /dev/null +++ b/config/default_records/tube_purposes/011_ultima_ug200_tube_purposes.wip.yml @@ -0,0 +1,14 @@ +--- +UPF2 EqVol Norm: + # Even though this is Ultima, not Illumina, the MX tube has the same behaviour + type: IlluminaHtp::MxTubePurpose + target_type: MultiplexedLibraryTube + stock_plate: false +UPF2 Balanced Pool: + type: IlluminaHtp::InitialStockTubePurpose + target_type: LibraryTube + stock_plate: false +UPF2 Balanced Norm: + type: IlluminaHtp::MxTubePurpose + target_type: MultiplexedLibraryTube + stock_plate: false From 3a8b09ad53f35fcb03ff5405526f9e37c294bac6 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 22:47:12 +0100 Subject: [PATCH 40/44] test(ci): fix flaky tests and add missing assertions --- test/controllers/batches_controller_test.rb | 3 +++ test/lib/label_printer/plate_to_tube_test.rb | 7 ++++++- test/unit/batch_test.rb | 2 +- test/unit/event_test.rb | 4 ++-- test/unit/request_factory_test.rb | 2 +- test/unit/sequencing_qc_batch_test.rb | 3 +++ test/unit/tasks/plate_transfer_task_test.rb | 1 + test/unit/user_test.rb | 2 +- 8 files changed, 18 insertions(+), 6 deletions(-) diff --git a/test/controllers/batches_controller_test.rb b/test/controllers/batches_controller_test.rb index a109e47d21..a7810bbefd 100644 --- a/test/controllers/batches_controller_test.rb +++ b/test/controllers/batches_controller_test.rb @@ -257,6 +257,9 @@ class BatchesControllerTest < ActionController::TestCase "request_group_#{@plate.id}_#{@submission.id}_size": '1', commit: 'Submit' } + + assert_redirected_to batch_path(assigns(:batch)) + assert_predicate assigns(:batch), :persisted? end end diff --git a/test/lib/label_printer/plate_to_tube_test.rb b/test/lib/label_printer/plate_to_tube_test.rb index 49bb24fbb4..6a289237cc 100644 --- a/test/lib/label_printer/plate_to_tube_test.rb +++ b/test/lib/label_printer/plate_to_tube_test.rb @@ -13,7 +13,12 @@ def setup # rubocop:todo Metrics/AbcSize @barcode1 = '1111' @asset_name = 'tube name' @tube1 = create(:sample_tube, :tube_barcode, barcode: barcode1, prefix: prefix, name: asset_name) - @sample_tubes = create_list(:sample_tube, 4, :tube_barcode) + @sample_tubes = [ + create(:sample_tube, :tube_barcode, barcode: '2222', prefix: prefix), + create(:sample_tube, :tube_barcode, barcode: '3333', prefix: prefix), + create(:sample_tube, :tube_barcode, barcode: '4444', prefix: prefix), + create(:sample_tube, :tube_barcode, barcode: '5555', prefix: prefix) + ] sample_tubes.unshift(tube1) options = { sample_tubes: } @tube_label = LabelPrinter::Label::PlateToTubes.new(options) diff --git a/test/unit/batch_test.rb b/test/unit/batch_test.rb index abfc223aef..e9d8ae10a5 100644 --- a/test/unit/batch_test.rb +++ b/test/unit/batch_test.rb @@ -647,7 +647,7 @@ class BatchTest < ActiveSupport::TestCase end should 'check that with the pipeline that the batch is valid' do - @batch.complete!(@user) + assert_nothing_raised { @batch.complete!(@user) } end end diff --git a/test/unit/event_test.rb b/test/unit/event_test.rb index 67e6a550e7..68fe62686c 100644 --- a/test/unit/event_test.rb +++ b/test/unit/event_test.rb @@ -18,7 +18,7 @@ class EventTest < ActiveSupport::TestCase setup { @event = Event.create(descriptor_key: '') } should 'be valid' do - @event.valid? + assert_predicate @event, :valid? end end end @@ -28,7 +28,7 @@ class EventTest < ActiveSupport::TestCase setup { @event = Event.create } should 'be valid' do - @event.valid? + assert_predicate @event, :valid? end end end diff --git a/test/unit/request_factory_test.rb b/test/unit/request_factory_test.rb index 5339e13433..538b6eaacc 100644 --- a/test/unit/request_factory_test.rb +++ b/test/unit/request_factory_test.rb @@ -42,7 +42,7 @@ class RequestcreateTest < ActiveSupport::TestCase setup { @project.update!(enforce_quotas: true) } should 'not fail' do - RequestFactory.copy_request(@request) + assert_nothing_raised { RequestFactory.copy_request(@request) } end end end diff --git a/test/unit/sequencing_qc_batch_test.rb b/test/unit/sequencing_qc_batch_test.rb index 89a123a9bb..eda3f8a081 100644 --- a/test/unit/sequencing_qc_batch_test.rb +++ b/test/unit/sequencing_qc_batch_test.rb @@ -38,6 +38,7 @@ class SequencingQcBatchTest < ActiveSupport::TestCase should 'do nothing if the next state is nil' do @batch.stubs(:qc_next_state).returns(nil) + @batch.expects(:update_attribute).never # no state update when there is no next state @batch.qc_submitted end end @@ -51,6 +52,7 @@ class SequencingQcBatchTest < ActiveSupport::TestCase should 'do nothing if the next state is nil' do @batch.stubs(:qc_next_state).returns(nil) + @batch.expects(:update_attribute).never # no state update when there is no next state @batch.qc_criteria_received end end @@ -64,6 +66,7 @@ class SequencingQcBatchTest < ActiveSupport::TestCase should 'do nothing if the next state is nil' do @batch.stubs(:qc_next_state).returns(nil) + @batch.expects(:update_attribute).never # no state update when there is no next state @batch.qc_complete end end diff --git a/test/unit/tasks/plate_transfer_task_test.rb b/test/unit/tasks/plate_transfer_task_test.rb index 9ed5ba55f8..669a0df93d 100644 --- a/test/unit/tasks/plate_transfer_task_test.rb +++ b/test/unit/tasks/plate_transfer_task_test.rb @@ -100,6 +100,7 @@ class PlateTransferTaskTest < ActiveSupport::TestCase end should 'find the existing plate' do + assert_equal 1, Plate.count - @plate_count end end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 126e8414d0..081be0ac56 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -123,7 +123,7 @@ class UserTest < ActiveSupport::TestCase should 'be able to have one assigned' do code = 'code' - @user.swipecard_code = code + assert_nothing_raised { @user.swipecard_code = code } end end From 4ebff56ed233c1884ab4b970e1a3eb4c58138486 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 9 Apr 2026 15:01:57 +0100 Subject: [PATCH 41/44] docs(sample_sheets): clarify checking most specific pipeline class first --- app/controllers/batches_controller.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/controllers/batches_controller.rb b/app/controllers/batches_controller.rb index e8afe27aca..4549d403f5 100644 --- a/app/controllers/batches_controller.rb +++ b/app/controllers/batches_controller.rb @@ -387,6 +387,10 @@ def find_batch_by_barcode # the batch. Ultima sample sheets are allowed to be downloaded without # authentication. For all other pipelines, the user must be logged in. # + # @note When checking the pipeline class, always check the most specific + # subclass first in the inheritance hierarchy, before considering parent + # classes. + # # @return [Boolean] true if download is allowed, false otherwise def allow_sample_sheet_download? return true if @batch.pipeline.is_a?(UltimaUG200SequencingPipeline) @@ -396,6 +400,11 @@ def allow_sample_sheet_download? end # Generates and sends the appropriate sample sheet(s) for the batch. + # + # @note When checking the pipeline class, always check the most specific + # subclass first in the inheritance hierarchy, before considering parent + # classes. + # # @return [void] def generate_sample_sheet return redirect_to(login_path) unless allow_sample_sheet_download? From f8397ad45e310b6c3ec11a330b618c630da94b7d Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 9 Apr 2026 15:29:52 +0100 Subject: [PATCH 42/44] Update config/default_records/request_types/026_ultima_ug200_request_types.wip.yml Co-authored-by: KatyTaylor --- .../request_types/026_ultima_ug200_request_types.wip.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml b/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml index bde1c5fc6e..bc5977f439 100644 --- a/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml +++ b/config/default_records/request_types/026_ultima_ug200_request_types.wip.yml @@ -26,7 +26,6 @@ ultima_ug200_sequencing: order: 2 initial_state: pending billable: true - # Using same product line name as Ultima UG100 product_line_name: Ultima # same as UG100 request_class_name: UltimaUG200SequencingRequest request_purpose: standard From 8cb2478dd5f35692055b3246c1f31804a76f425c Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 9 Apr 2026 15:42:13 +0100 Subject: [PATCH 43/44] refactor(sample_sheets): rename variable tg to tag_group for readability in tag_index_map method --- .../ultima_sample_sheet/sample_sheet_generator.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb index 3d9810971f..86f54aa5e0 100644 --- a/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb +++ b/app/controllers/ultima_sample_sheet/sample_sheet_generator.rb @@ -223,9 +223,9 @@ def study_id_for(aliquot) # other tag groups exist in the database. # @return [Hash{Tag => Integer}] mapping of tags to index numbers def tag_index_map - @tag_index_map ||= ultima_tag_groups.each_with_object({}) do |tg, map| - start_index = ultima_tag_groups_config[tg.name][:z_start] - tg.tags.sort_by(&:map_id).each_with_index { |tag, i| map[tag] = start_index + i } + @tag_index_map ||= ultima_tag_groups.each_with_object({}) do |tag_group, map| + start_index = ultima_tag_groups_config[tag_group.name][:z_start] + tag_group.tags.sort_by(&:map_id).each_with_index { |tag, i| map[tag] = start_index + i } end end From e429b9895c39a88689d8c0f1991b4ca8a0d66171 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 9 Apr 2026 15:58:29 +0100 Subject: [PATCH 44/44] refactor: remove redundant method and constant from UltimaUG200Validator --- app/validators/ultima_ug200_validator.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/validators/ultima_ug200_validator.rb b/app/validators/ultima_ug200_validator.rb index 1dc5c9889e..ae66f898ad 100644 --- a/app/validators/ultima_ug200_validator.rb +++ b/app/validators/ultima_ug200_validator.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UltimaUG200Validator < UltimaValidator WAFER_SIZE_CONSISTENT_MSG = 'Wafer size must be the same for both requests.' - OT_RECIPE_CONSISTENT_MSG = 'OT Recipe must be the same for both requests.' # Used in _pipeline_limit.html to display custom validation warnings def self.validation_info @@ -22,10 +21,4 @@ def requests_have_same_wafer_size(record) record.errors.add(:base, WAFER_SIZE_CONSISTENT_MSG) end - - def requests_have_same_ot_recipe(record) - return if record.pipeline.ot_recipe_consistent_for_batch?(record) - - record.errors.add(:base, OT_RECIPE_CONSISTENT_MSG) - end end