From aa0b7ce8de838a5205c54aa06842b6ca7b931432 Mon Sep 17 00:00:00 2001 From: Andrew Sparkes Date: Fri, 20 Mar 2026 14:40:57 +0000 Subject: [PATCH 001/125] 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 822d5f4ce79ea90d2aedd755da54139c2afe5d23 Mon Sep 17 00:00:00 2001 From: Katy Taylor Date: Mon, 23 Mar 2026 14:37:25 +0000 Subject: [PATCH 002/125] Add ability to specify the Project when creating an Order through the API --- app/controllers/api/v2/orders_controller.rb | 11 +++++++++++ app/models/order.rb | 6 +++++- app/models/submission_template.rb | 3 ++- app/resources/api/v2/order_resource.rb | 9 ++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v2/orders_controller.rb b/app/controllers/api/v2/orders_controller.rb index aa08419060..db8e337d04 100644 --- a/app/controllers/api/v2/orders_controller.rb +++ b/app/controllers/api/v2/orders_controller.rb @@ -17,6 +17,17 @@ class OrderProcessor < JSONAPI::Processor def prepare_context context[:template] = find_template context[:template_attributes] = template_attributes unless context[:template].nil? + context[:project] = find_project + end + + def find_project + project_uuid = params[:data][:attributes][:project_uuid] + return nil if project_uuid.nil? + + project = Project.with_uuid(project_uuid).first + raise JSONAPI::Exceptions::InvalidFieldValue.new(:project_uuid, project_uuid) if project.nil? + + project end def find_template diff --git a/app/models/order.rb b/app/models/order.rb index b59a349d9d..244e7eefbe 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -61,7 +61,11 @@ class Order < ApplicationRecord # rubocop:todo Metrics/ClassLength serialize :item_options, coder: YAML before_validation :set_study_from_aliquots, unless: :cross_study_allowed, if: :autodetect_studies - before_validation :set_project_from_aliquots, unless: :cross_project_allowed, if: :autodetect_projects + before_validation :set_project_from_aliquots, unless: :cross_project_allowed, if: [:autodetect_projects, :project_not_set] + + def project_not_set + project.blank? + end validates :study, presence: true, unless: :cross_study_allowed validates :project, presence: true, unless: :cross_project_allowed diff --git a/app/models/submission_template.rb b/app/models/submission_template.rb index dd25355107..dafdd6911d 100644 --- a/app/models/submission_template.rb +++ b/app/models/submission_template.rb @@ -54,8 +54,9 @@ def supercede end end - def create_order!(attributes) + def create_order!(attributes, project = nil) new_order(attributes).tap do |order| + order.project = project if project yield(order) if block_given? order.save! end diff --git a/app/resources/api/v2/order_resource.rb b/app/resources/api/v2/order_resource.rb index aa1a935920..e2f19f8725 100644 --- a/app/resources/api/v2/order_resource.rb +++ b/app/resources/api/v2/order_resource.rb @@ -53,6 +53,8 @@ module V2 # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) # or refer to the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation. class OrderResource < BaseResource + + ### # Attributes ### @@ -75,6 +77,11 @@ class OrderResource < BaseResource # @return [String] The UUID of this {Order}. attribute :uuid, readonly: true + # @!attribute [w] project_uuid + # @return [String] The UUID of the project to associate with this Order on creation. + attribute :project_uuid, writeonly: true + attr_writer :project_uuid # Not stored, consumed by OrderProcessor. + ### # Relationships ### @@ -131,7 +138,7 @@ class OrderResource < BaseResource def self.create(context) return super if context[:template].nil? - order = context[:template].create_order!(context[:template_attributes]) + order = context[:template].create_order!(context[:template_attributes], context[:project]) new(order, context) 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 003/125] 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 004/125] 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 005/125] 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 006/125] 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 007/125] 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 008/125] 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 009/125] 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 010/125] 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 011/125] 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 012/125] 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 013/125] 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 bb460c9fb67f29db9790ed966de47f4430ad0d7f Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Mar 2026 01:28:00 +0100 Subject: [PATCH 014/125] fix: track task execution to drive redirect behavior --- app/controllers/workflows_controller.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/workflows_controller.rb b/app/controllers/workflows_controller.rb index 8495e54ae5..ff3b9a98d4 100644 --- a/app/controllers/workflows_controller.rb +++ b/app/controllers/workflows_controller.rb @@ -41,6 +41,11 @@ def stage # rubocop:todo Metrics/CyclomaticComplexity @stage = params[:id].to_i @task = @workflow.tasks[@stage] + # Track whether the current task execution succeeded; defaults to true when + # just rendering so we only redirect on "Update" after a successful do_task + # call. + task_success = true + # If params[:next_stage] is nil then just render the current task # else actually execute the task. unless params[:next_stage].nil? @@ -67,7 +72,7 @@ def stage # rubocop:todo Metrics/CyclomaticComplexity end end - if params[:commit] == 'Update' + if params[:commit] == 'Update' && task_success redirect_to batch_path(@batch) elsif @stage >= @workflow.tasks.size # All requests have finished all tasks: finish workflow From cbef03ee306f135957b0313c74b4605853cc7185 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Mar 2026 02:03:28 +0100 Subject: [PATCH 015/125] feat: validate required and date-based inputs --- app/models/descriptor.rb | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/app/models/descriptor.rb b/app/models/descriptor.rb index ed37873ea1..7dc9e9d447 100644 --- a/app/models/descriptor.rb +++ b/app/models/descriptor.rb @@ -3,6 +3,9 @@ class Descriptor < ApplicationRecord belongs_to :task serialize :selection, coder: YAML + DATE_YEAR_MIN = 1990 + DATE_YEAR_MAX = 2100 + def is_required? required end @@ -11,4 +14,40 @@ def matches?(search) search.descriptors.each { |descriptor| return true if descriptor.name == name && descriptor.value == value } false end + + # Returns an array of validation errors for the submitted descriptor value. + # The value comes from the Task Details form for a workflow task on a batch. + # @return [Array] An array of error messages, empty if the value is valid + def validate_value(submitted_value) + return ["#{name} is required"] if submitted_value.blank? && is_required? + return [] if submitted_value.blank? + return validate_date_value(submitted_value) if kind == 'Date' + + [] + end + + private + + # Validates that the submitted value is a valid date string in the format + # YYYY-MM-DD, and that the year is within a reasonable range. + # @return [Array] An array of error messages, empty if the value is valid + def validate_date_value(submitted_value) + unless submitted_value.match?(/\A\d{4}-\d{2}-\d{2}\z/) + return ["'#{submitted_value}' is not a valid date for #{name} (expected YYYY-MM-DD)"] + end + + parsed_date = Date.iso8601(submitted_value) + validate_date_year(parsed_date) + rescue ArgumentError + ["'#{submitted_value}' is not a valid date for #{name} (expected YYYY-MM-DD)"] + end + + # Validates that the year of the submitted date is within a reasonable range + # to catch common data entry errors (e.g. 62026 instead of 2026). + # @return [Array] An array of error messages, empty if the year is valid + def validate_date_year(parsed_date) + return [] if parsed_date.year.between?(DATE_YEAR_MIN, DATE_YEAR_MAX) + + ["Date year for #{name} must be between #{DATE_YEAR_MIN} and #{DATE_YEAR_MAX} (got #{parsed_date.year})"] + end end From 05a4146bad4de7de69248fe8bd9c1d86e646468c Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Mar 2026 02:14:38 +0100 Subject: [PATCH 016/125] feat: validate task details inputs in set descriptors handler --- app/models/tasks/set_descriptors_handler.rb | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/models/tasks/set_descriptors_handler.rb b/app/models/tasks/set_descriptors_handler.rb index 62a501e45d..7ae7e56005 100644 --- a/app/models/tasks/set_descriptors_handler.rb +++ b/app/models/tasks/set_descriptors_handler.rb @@ -8,6 +8,9 @@ def render end def perform + errors = validate_descriptor_inputs + return [false, errors.join(', ')] if errors.any? + # Process each request that has been selected in the front end # by default all requests are selected, but in rare circumstances the user # can uncheck a request to exclude it from the step @@ -28,10 +31,39 @@ def perform private + # Iterates every descriptor on the task and asks each one to validate the + # submitted value. + # @return [Array] An array of error messages, empty if all values are valid + def validate_descriptor_inputs + return [] if task.descriptors.empty? + + selected_requests_for_validation.each_with_object([]) do |request, errors| + errors.concat(descriptor_errors_for(request)) + end.uniq + end + def params @params.respond_to?(:permit!) ? @params.permit!.to_h : @params end + # Returns an array of the requests that have been selected in the UI for + # the Task Details form. + # @return [Array] An array of selected requests + def selected_requests_for_validation + requests.select { |request| selected_requests.include?(request.id) } + end + + # For a given request, returns an array of error messages for any descriptors + # that fail validation. + # @return [Array] An array of error messages, empty if all values are valid + def descriptor_errors_for(request) + submitted = descriptors(request) + + task.descriptors.each_with_object([]) do |descriptor, errors| + errors.concat(descriptor.validate_value(submitted[descriptor.name])) + end + end + def process_request(request) LabEvent.create!( batch: batch, From 71ac87872dc79d32cd516295fb2d715bc73e3928 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Mar 2026 02:19:57 +0100 Subject: [PATCH 017/125] test: add descriptor required and date validation tests --- spec/models/descriptor_spec.rb | 127 +++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 spec/models/descriptor_spec.rb diff --git a/spec/models/descriptor_spec.rb b/spec/models/descriptor_spec.rb new file mode 100644 index 0000000000..296041e233 --- /dev/null +++ b/spec/models/descriptor_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Descriptor do + describe '#validate_value' do + subject(:errors) { descriptor.validate_value(value) } + + context 'when kind is Date' do + context 'when required is true' do + let(:descriptor) { described_class.new(name: 'OTR carrier expiry', kind: 'Date', required: true) } + + context 'with a valid ISO 8601 date' do + let(:value) { '2026-06-01' } + + it { is_expected.to be_empty } + end + + context 'with a blank value' do + let(:value) { '' } + + it { is_expected.to contain_exactly('OTR carrier expiry is required') } + end + + context 'with an invalid date string' do + let(:value) { 'not-a-date' } + + it { + is_expected.to contain_exactly( + "'not-a-date' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" + ) + } + end + + context 'with a date in the wrong format (DD/MM/YYYY)' do + let(:value) { '01/06/2026' } + + it { + is_expected.to contain_exactly( + "'01/06/2026' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" + ) + } + end + + context 'with a year too far in the past' do + let(:value) { '1989-06-01' } + + it { + is_expected.to contain_exactly( + 'Date year for OTR carrier expiry must be between 1990 and 2100 (got 1989)' + ) + } + end + + context 'with a year too far in the future' do + let(:value) { '2101-06-01' } + + it { + is_expected.to contain_exactly( + 'Date year for OTR carrier expiry must be between 1990 and 2100 (got 2101)' + ) + } + end + + context 'with a typo in the year (e.g., 62026 instead of 2026)' do + let(:value) { '62026-06-01' } + + it { + is_expected.to contain_exactly( + "'62026-06-01' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" + ) + } + end + end + + context 'when required is false' do + let(:descriptor) { described_class.new(name: 'OTR carrier expiry', kind: 'Date', required: false) } + + context 'with a blank value' do + let(:value) { '' } + + it { is_expected.to be_empty } + end + + context 'with a valid ISO 8601 date' do + let(:value) { '2026-06-01' } + + it { is_expected.to be_empty } + end + + context 'with an invalid date string' do + let(:value) { 'not-a-date' } + + it { + is_expected.to contain_exactly( + "'not-a-date' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" + ) + } + end + + context 'with a year sanity check failure' do + let(:value) { '62026-06-01' } + + it { + is_expected.to contain_exactly( + "'62026-06-01' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" + ) + } + end + end + end + + context 'when kind is Text' do + let(:descriptor) { described_class.new(name: 'Comment', kind: 'Text', required: false) } + let(:value) { 'any free text' } + + it { is_expected.to be_empty } + end + + context 'when kind is Selection' do + let(:descriptor) { described_class.new(name: 'Workflow', kind: 'Selection', required: false) } + let(:value) { 'Standard' } + + it { is_expected.to be_empty } + end + end +end From 623db61f0e7629d5128cc67857f51af8f7470594 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 30 Mar 2026 02:25:24 +0100 Subject: [PATCH 018/125] test: add handler tests for descriptor validations --- .../set_descriptors_handler/handler_spec.rb | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/spec/models/tasks/set_descriptors_handler/handler_spec.rb b/spec/models/tasks/set_descriptors_handler/handler_spec.rb index 816d97a1ec..d247a7f66c 100644 --- a/spec/models/tasks/set_descriptors_handler/handler_spec.rb +++ b/spec/models/tasks/set_descriptors_handler/handler_spec.rb @@ -11,7 +11,7 @@ let(:request) { batch.requests.first } let(:controller) { instance_double(WorkflowsController) } let(:user) { create(:user) } - let(:task) { instance_double(SetDescriptorsTask, name: 'Step 1', id: 1) } + let(:task) { instance_double(SetDescriptorsTask, name: 'Step 1', id: 1, descriptors: []) } describe '#perform' do context 'with all requests selected' do @@ -46,5 +46,48 @@ ) end end + + # The handler delegates validation entirely to Descriptor#validate_value. + # Full Date validation rules are covered in spec/models/descriptor_spec.rb. + context 'when a descriptor returns no errors' do + let(:passing_descriptor) { instance_double(Descriptor, name: 'OTR carrier expiry') } + let(:task) { instance_double(SetDescriptorsTask, name: 'Step 1', id: 1, descriptors: [passing_descriptor]) } + let(:params) do + { + batch_id: batch.id.to_s, + descriptors: { 'OTR carrier expiry' => '2026-06-01' }, + request: { request.id.to_s => 'on' } + } + end + + before { allow(passing_descriptor).to receive(:validate_value).with('2026-06-01').and_return([]) } + + it 'returns true' do + expect(handler.perform).to be true + end + end + + context 'when a descriptor returns a validation error' do + let(:error_message) { "'not-a-date' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" } + let(:failing_descriptor) { instance_double(Descriptor, name: 'OTR carrier expiry') } + let(:task) { instance_double(SetDescriptorsTask, name: 'Step 1', id: 1, descriptors: [failing_descriptor]) } + let(:params) do + { + batch_id: batch.id.to_s, + descriptors: { 'OTR carrier expiry' => 'not-a-date' }, + request: { request.id.to_s => 'on' } + } + end + + before { allow(failing_descriptor).to receive(:validate_value).with('not-a-date').and_return([error_message]) } + + it 'returns [false, error_message]' do + expect(handler.perform).to eq([false, error_message]) + end + + it 'does not create any lab events' do + expect { handler.perform }.not_to change(LabEvent, :count) + end + end end end From 365c2f0aaa895936b1b3b5eab4545577c1dbe00f Mon Sep 17 00:00:00 2001 From: Wendy Yang Date: Mon, 30 Mar 2026 22:48:47 +0100 Subject: [PATCH 019/125] fix the linting --- app/models/order.rb | 3 ++- app/models/submission_template.rb | 2 ++ app/resources/api/v2/order_resource.rb | 2 -- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/models/order.rb b/app/models/order.rb index 244e7eefbe..6ef2f20b29 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -61,7 +61,8 @@ class Order < ApplicationRecord # rubocop:todo Metrics/ClassLength serialize :item_options, coder: YAML before_validation :set_study_from_aliquots, unless: :cross_study_allowed, if: :autodetect_studies - before_validation :set_project_from_aliquots, unless: :cross_project_allowed, if: [:autodetect_projects, :project_not_set] + before_validation :set_project_from_aliquots, unless: :cross_project_allowed, + if: %i[autodetect_projects project_not_set] def project_not_set project.blank? diff --git a/app/models/submission_template.rb b/app/models/submission_template.rb index dafdd6911d..c1a362b4b6 100644 --- a/app/models/submission_template.rb +++ b/app/models/submission_template.rb @@ -3,6 +3,7 @@ # We could have use a Prototype Factory , and so just associate a name to existing submission # but that doesn't work because the submission prototype doesn't pass the validation stage. # Anyway that's basically a prototype factory +# rubocop:disable Metrics/ClassLength class SubmissionTemplate < ApplicationRecord include Uuid::Uuidable @@ -147,3 +148,4 @@ def safely_duplicate(params) # rubocop:todo Metrics/MethodLength end end end +# rubocop:enable Metrics/ClassLength diff --git a/app/resources/api/v2/order_resource.rb b/app/resources/api/v2/order_resource.rb index e2f19f8725..49cb774279 100644 --- a/app/resources/api/v2/order_resource.rb +++ b/app/resources/api/v2/order_resource.rb @@ -53,8 +53,6 @@ module V2 # For more information about JSON:API see the [JSON:API Specifications](https://jsonapi.org/format/) # or refer to the [JSONAPI::Resources](http://jsonapi-resources.com/) package for Sequencescape's implementation. class OrderResource < BaseResource - - ### # Attributes ### From fc4f47a683be807b23f8e1410d843fe989841279 Mon Sep 17 00:00:00 2001 From: Wendy Yang Date: Tue, 31 Mar 2026 08:57:17 +0100 Subject: [PATCH 020/125] fix linting --- app/models/submission_template.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/submission_template.rb b/app/models/submission_template.rb index f76b3b7ee6..6adeffa9bb 100644 --- a/app/models/submission_template.rb +++ b/app/models/submission_template.rb @@ -151,4 +151,3 @@ def safely_duplicate(params) # rubocop:todo Metrics/MethodLength end end end -# rubocop:enable Metrics/ClassLength From 1f6fc3359740ce7d46dd4bfa6f58808dfa234873 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 00:00:52 +0000 Subject: [PATCH 021/125] Update Node.js to version 24.14.1 --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index d845d9d88d..8e35034890 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.14.0 +24.14.1 From 7c1fd8e0841791b6f7885f5b41f5584591fee407 Mon Sep 17 00:00:00 2001 From: Wendy Yang Date: Wed, 1 Apr 2026 10:37:44 +0100 Subject: [PATCH 022/125] fix the test --- spec/resources/api/v2/order_resource_spec.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/resources/api/v2/order_resource_spec.rb b/spec/resources/api/v2/order_resource_spec.rb index 5428ac9f7e..71eacee316 100644 --- a/spec/resources/api/v2/order_resource_spec.rb +++ b/spec/resources/api/v2/order_resource_spec.rb @@ -16,6 +16,7 @@ it { is_expected.to have_readonly_attribute :request_options } it { is_expected.to have_readonly_attribute :request_types } it { is_expected.to have_readonly_attribute :uuid } + it { is_expected.to have_writeonly_attribute :project_uuid } # Relationships it { is_expected.to have_a_readonly_has_one(:project).with_class_name('Project') } @@ -39,11 +40,14 @@ end context 'with a template in the context' do - let(:context) { { template:, template_attributes: } } + let(:context) { { template:, template_attributes:, project: } } let(:template) { instance_double(SubmissionTemplate) } let(:template_attributes) { {} } + let(:project) { build_stubbed(:project) } - before { allow(template).to receive(:create_order!).with(template_attributes).and_return(resource_model) } + before do + allow(template).to receive(:create_order!).with(template_attributes, project).and_return(resource_model) + end it 'does not call create on the super class' do allow(described_class.superclass).to receive(:create) From 616038139fd6050cccc43ed1e5c34942e1862536 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 12:01:26 +0100 Subject: [PATCH 023/125] 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 024/125] 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 025/125] 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 026/125] 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 027/125] 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 028/125] 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 5fea184c844b2f8d4debd9b6b9c6a2f00abb1492 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:43:02 +0000 Subject: [PATCH 029/125] Update flipper to version 1.4.1 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e2536e3eb8..9e72f1f98d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -247,7 +247,7 @@ GEM ffi (1.17.3-arm64-darwin) ffi (1.17.3-x86_64-darwin) ffi (1.17.3-x86_64-linux-gnu) - flipper (1.4.0) + flipper (1.4.1) concurrent-ruby (< 2) flipper-active_record (1.4.0) activerecord (>= 4.2, < 9) From 49f041a13422504db0d3b99eab27a46f31d2f2e0 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:09:18 +0000 Subject: [PATCH 030/125] Update flipper-active_record to version 1.4.1 --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9e72f1f98d..193a98ae32 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -249,9 +249,9 @@ GEM ffi (1.17.3-x86_64-linux-gnu) flipper (1.4.1) concurrent-ruby (< 2) - flipper-active_record (1.4.0) + flipper-active_record (1.4.1) activerecord (>= 4.2, < 9) - flipper (~> 1.4.0) + flipper (~> 1.4.1) flipper-ui (1.4.0) erubi (>= 1.0.0, < 2.0.0) flipper (~> 1.4.0) From 4c589434708f46f463f20b6495bc7d477beb309d Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 2 Apr 2026 15:33:35 +0100 Subject: [PATCH 031/125] 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 032/125] 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 033/125] 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 034/125] 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 035/125] 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 036/125] 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 037/125] 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 038/125] 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 aea5c162219b880ed47b3e6f6dd1cb51006361bf Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:10:19 +0000 Subject: [PATCH 039/125] Update flipper-ui to version 1.4.1 --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 193a98ae32..80899bcfd8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -252,9 +252,9 @@ GEM flipper-active_record (1.4.1) activerecord (>= 4.2, < 9) flipper (~> 1.4.1) - flipper-ui (1.4.0) + flipper-ui (1.4.1) erubi (>= 1.0.0, < 2.0.0) - flipper (~> 1.4.0) + flipper (~> 1.4.1) rack (>= 1.4, < 4) rack-protection (>= 1.5.3, < 5.0.0) rack-session (>= 1.0.2, < 3.0.0) @@ -391,7 +391,7 @@ GEM puma (7.2.0) nio4r (~> 2.0) racc (1.8.1) - rack (2.2.22) + rack (2.2.23) rack-acceptable (0.1.0) rack (>= 1.1.0) rack-cors (2.0.2) From b066237556f092987f37823bceba063b206f6758 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 19:00:20 +0000 Subject: [PATCH 040/125] Update json to version 2.19.3 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 193a98ae32..1e8a2902ea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -283,7 +283,7 @@ GEM prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.19.2) + json (2.19.3) json-schema (6.2.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) From be3a5b4bc41be1a08249b2b29441a7212228d87f Mon Sep 17 00:00:00 2001 From: Wendy Yang Date: Tue, 7 Apr 2026 13:16:49 +0100 Subject: [PATCH 041/125] remove the default project --- .../020_lcm_triomics_new_added_submission_templates.wip.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml b/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml index 824e90cde6..ffb579ff1a 100644 --- a/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml +++ b/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml @@ -6,7 +6,7 @@ Limber-Htp - LCM Triomics EMSeq: request_type_keys: ["limber_lcm_triomics_emseq"] product_line_name: LCM Triomics product_catalogue_name: LCM Triomics - project_name: "UAT Project" + # project_name: "UAT Project" # LCM Triomics RNASeq submission template Limber-Htp - LCM Triomics RNASeq: @@ -15,4 +15,4 @@ Limber-Htp - LCM Triomics RNASeq: request_type_keys: ["limber_lcm_triomics_rnaseq"] product_line_name: LCM Triomics product_catalogue_name: LCM Triomics - project_name: "UAT Project" + # project_name: "UAT Project" From 5dbd4a929eba9cea768464a7e8adb751f8bd4cd4 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 7 Apr 2026 13:23:30 +0100 Subject: [PATCH 042/125] 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 043/125] 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 044/125] 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 045/125] 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 046/125] 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 047/125] 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 048/125] 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 049/125] 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 050/125] 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 051/125] 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 052/125] 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 053/125] 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 054/125] 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 055/125] 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 7cf36987b87fb4f844ed5eb77088ce640fa8ca62 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 02:08:58 +0000 Subject: [PATCH 056/125] Update addressable to version 2.9.0 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 80899bcfd8..62fa1fc908 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -121,7 +121,7 @@ GEM minitest (>= 5.1, < 6) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - addressable (2.8.9) + addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) after_commit_everywhere (1.6.0) activerecord (>= 4.2) From 2ac0821cec148661ec1d400e012dc682697eb9f1 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 8 Apr 2026 14:25:08 +0100 Subject: [PATCH 057/125] build: update to Rails 8.0 --- Gemfile | 2 +- Gemfile.lock | 118 +++++++++++++++++++++++++-------------------------- 2 files changed, 58 insertions(+), 62 deletions(-) diff --git a/Gemfile b/Gemfile index 8cf19d6709..e8fd7fba0e 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ group :default do gem 'concurrent-ruby', '1.3.5' gem 'configatron' gem 'formtastic' - gem 'rails', '~> 7.2.0' + gem 'rails', '~> 8.0.0' # Previously part of ruby or rails, now separate gems gem 'drb' diff --git a/Gemfile.lock b/Gemfile.lock index 80899bcfd8..1cd8031217 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,33 +43,31 @@ GEM specs: aasm (5.5.2) concurrent-ruby (~> 1.0) - actioncable (7.2.3.1) - actionpack (= 7.2.3.1) - activesupport (= 7.2.3.1) + actioncable (8.0.5) + actionpack (= 8.0.5) + activesupport (= 8.0.5) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.3.1) - actionpack (= 7.2.3.1) - activejob (= 7.2.3.1) - activerecord (= 7.2.3.1) - activestorage (= 7.2.3.1) - activesupport (= 7.2.3.1) + actionmailbox (8.0.5) + actionpack (= 8.0.5) + activejob (= 8.0.5) + activerecord (= 8.0.5) + activestorage (= 8.0.5) + activesupport (= 8.0.5) mail (>= 2.8.0) - actionmailer (7.2.3.1) - actionpack (= 7.2.3.1) - actionview (= 7.2.3.1) - activejob (= 7.2.3.1) - activesupport (= 7.2.3.1) + actionmailer (8.0.5) + actionpack (= 8.0.5) + actionview (= 8.0.5) + activejob (= 8.0.5) + activesupport (= 8.0.5) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.3.1) - actionview (= 7.2.3.1) - activesupport (= 7.2.3.1) - cgi + actionpack (8.0.5) + actionview (= 8.0.5) + activesupport (= 8.0.5) nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4, < 3.3) + rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) @@ -78,38 +76,37 @@ GEM actionpack-xml_parser (2.0.1) actionpack (>= 5.0) railties (>= 5.0) - actiontext (7.2.3.1) - actionpack (= 7.2.3.1) - activerecord (= 7.2.3.1) - activestorage (= 7.2.3.1) - activesupport (= 7.2.3.1) + actiontext (8.0.5) + actionpack (= 8.0.5) + activerecord (= 8.0.5) + activestorage (= 8.0.5) + activesupport (= 8.0.5) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.3.1) - activesupport (= 7.2.3.1) + actionview (8.0.5) + activesupport (= 8.0.5) builder (~> 3.1) - cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.2.3.1) - activesupport (= 7.2.3.1) + activejob (8.0.5) + activesupport (= 8.0.5) globalid (>= 0.3.6) - activemodel (7.2.3.1) - activesupport (= 7.2.3.1) - activerecord (7.2.3.1) - activemodel (= 7.2.3.1) - activesupport (= 7.2.3.1) + activemodel (8.0.5) + activesupport (= 8.0.5) + activerecord (8.0.5) + activemodel (= 8.0.5) + activesupport (= 8.0.5) timeout (>= 0.4.0) activerecord-import (2.2.0) activerecord (>= 4.2) - activestorage (7.2.3.1) - actionpack (= 7.2.3.1) - activejob (= 7.2.3.1) - activerecord (= 7.2.3.1) - activesupport (= 7.2.3.1) + activestorage (8.0.5) + actionpack (= 8.0.5) + activejob (= 8.0.5) + activerecord (= 8.0.5) + activesupport (= 8.0.5) marcel (~> 1.0) - activesupport (7.2.3.1) + activesupport (8.0.5) base64 benchmark (>= 0.3) bigdecimal @@ -118,9 +115,10 @@ GEM drb i18n (>= 1.6, < 2) logger (>= 1.4.2) - minitest (>= 5.1, < 6) + minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.8.9) public_suffix (>= 2.0.2, < 8.0) after_commit_everywhere (1.6.0) @@ -167,7 +165,6 @@ GEM marcel (~> 1.0) nokogiri (~> 1.10, >= 1.10.4) rubyzip (>= 2.4, < 4) - cgi (0.5.1) childprocess (5.1.0) logger (~> 1.5) choice (0.2.0) @@ -410,20 +407,20 @@ GEM rackup (1.0.1) rack (< 3) webrick - rails (7.2.3.1) - actioncable (= 7.2.3.1) - actionmailbox (= 7.2.3.1) - actionmailer (= 7.2.3.1) - actionpack (= 7.2.3.1) - actiontext (= 7.2.3.1) - actionview (= 7.2.3.1) - activejob (= 7.2.3.1) - activemodel (= 7.2.3.1) - activerecord (= 7.2.3.1) - activestorage (= 7.2.3.1) - activesupport (= 7.2.3.1) + rails (8.0.5) + actioncable (= 8.0.5) + actionmailbox (= 8.0.5) + actionmailer (= 8.0.5) + actionpack (= 8.0.5) + actiontext (= 8.0.5) + actionview (= 8.0.5) + activejob (= 8.0.5) + activemodel (= 8.0.5) + activerecord (= 8.0.5) + activestorage (= 8.0.5) + activesupport (= 8.0.5) bundler (>= 1.15.0) - railties (= 7.2.3.1) + railties (= 8.0.5) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -441,10 +438,9 @@ GEM loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-perftest (0.0.7) - railties (7.2.3.1) - actionpack (= 7.2.3.1) - activesupport (= 7.2.3.1) - cgi + railties (8.0.5) + actionpack (= 8.0.5) + activesupport (= 8.0.5) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -729,7 +725,7 @@ DEPENDENCIES rack-acceptable rack-cors rack-mini-profiler - rails (~> 7.2.0) + rails (~> 8.0.0) rails-controller-testing rails-erd rails-perftest From 4bf300b396e37c00191679f68f3a48a7525b7c63 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Fri, 23 Jan 2026 11:59:07 +0000 Subject: [PATCH 058/125] fix: rename resources delete to destroy --- config/routes.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index e176e1843a..db4beaa56d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -438,7 +438,7 @@ member { get :show } end - resources :pipelines, except: [:delete] do + resources :pipelines, except: [:destroy] do collection { post :update_priority } member do get :reception @@ -485,7 +485,7 @@ resources :asset_audits - resources :qc_reports, except: %i[delete update] do + resources :qc_reports, except: %i[destroy update] do collection { post :qc_file } end From 44e685d0ccaf632205f63e4fb21f8a96b86b6214 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 8 Apr 2026 14:29:05 +0100 Subject: [PATCH 059/125] build: update Rails app bundle exec rails app:update --- bin/dev | 2 + bin/setup | 37 ++---- config/application.rb | 93 ++------------ config/boot.rb | 40 +----- config/environment.rb | 3 +- config/environments/development.rb | 87 +++---------- config/environments/production.rb | 91 +++++++++++++- config/environments/test.rb | 47 ++------ .../initializers/content_security_policy.rb | 78 ++++-------- .../initializers/filter_parameter_logging.rb | 6 +- config/initializers/inflections.rb | 4 +- .../new_framework_defaults_8_0.rb | 30 +++++ config/puma.rb | 57 ++------- public/400.html | 114 ++++++++++++++++++ public/404.html | 114 ++++++++++++++++++ public/406-unsupported-browser.html | 114 ++++++++++++++++++ public/422.html | 114 ++++++++++++++++++ public/500.html | 114 ++++++++++++++++++ public/icon.png | Bin 0 -> 4166 bytes public/icon.svg | 3 + 20 files changed, 779 insertions(+), 369 deletions(-) create mode 100755 bin/dev create mode 100644 config/initializers/new_framework_defaults_8_0.rb create mode 100644 public/400.html create mode 100644 public/404.html create mode 100644 public/406-unsupported-browser.html create mode 100644 public/422.html create mode 100644 public/500.html create mode 100644 public/icon.png create mode 100644 public/icon.svg diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000000..5f91c20545 --- /dev/null +++ b/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/bin/setup b/bin/setup index 26115bb2ef..be3db3c0d6 100755 --- a/bin/setup +++ b/bin/setup @@ -2,7 +2,6 @@ require "fileutils" APP_ROOT = File.expand_path("..", __dir__) -# APP_NAME set in lib/deployed.rb def system!(*args) system(*args, exception: true) @@ -12,30 +11,9 @@ FileUtils.chdir APP_ROOT do # This script is a way to set up or update your development environment automatically. # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. - puts '== Checking basics' - expected_version = (/[\d.]+/.match(File.read('.ruby-version')))[0] - unless RUBY_VERSION == expected_version - puts "Expected ruby #{expected_version} got #{RUBY_VERSION}" - puts "Please install ruby #{expected_version}" - puts 'You may want to install a ruby version manager' - puts 'https://github.com/rbenv/rbenv' - end puts "== Installing dependencies ==" - - # Set SKIP_AUTOMATIC_GEM_INSTALLATION to disable bundle install here on the CI - # suite as we've already done it, and this section gets a little clever. - unless ENV['SKIP_AUTOMATIC_GEM_INSTALLATION'] - system! 'gem install bundler --conservative' - - system('bundle check') || system!('bundle install --jobs 4 --retry 3') - end - - # Install JavaScript dependencies if using Yarn - system('bin/yarn') - - puts "\n== Setting credentials ==" - system 'bin/rails credentials:edit' + system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" # unless File.exist?("config/database.yml") @@ -43,15 +21,14 @@ FileUtils.chdir APP_ROOT do # end puts "\n== Preparing database ==" - system! "bin/rails db:setup" # db:prepare breaks the views_schema loading. TODO: Y25-282 + system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" - puts "\n== Restarting application server ==" - system! "bin/rails restart" - - # puts "\n== Configuring puma-dev ==" - # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}" - # system "curl -Is https://#{APP_NAME}.test/up | head -n 1" + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end end diff --git a/config/application.rb b/config/application.rb index 595437b4af..ef94e692b4 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,8 +1,6 @@ -# frozen_string_literal: true +require_relative "boot" -require_relative 'boot' - -require 'rails/all' +require "rails/all" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. @@ -13,92 +11,17 @@ class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.2 - # Settings in config/environments/* take precedence over those specified here. - # Application configuration can go into files in config/initializers - # -- all .rb files in that directory are automatically loaded after loading - # the framework and any gems in your application. - - # Configure the default encoding used in templates for Ruby 1.9. - config.encoding = 'utf-8' - - # Default options which predate the Rails 5 switch - # Due to loading order, set these here and not in an initializer, see https://github.com/rails/rails/issues/23589 - config.active_record.belongs_to_required_by_default = false - config.action_controller.forgery_protection_origin_check = false - config.action_controller.per_form_csrf_tokens = false - - # Enable YJIT by default if running Ruby 3.3+ - # YJIT is Ruby's JIT compiler that is available in CRuby since Ruby 3.1. - # It can provide significant performance improvements for Rails applications, offering 15-25% latency improvements. - # In Rails 7.2, YJIT is enabled by default if running Ruby 3.3 or newer. - # You can disable YJIT by setting: - # Rails.application.config.yjit = false - - # Sets the exceptions application invoked by the ShowException middleware when an exception happens. - config.exceptions_app = routes - - # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters += [:password] - config.logger = Logger.new(Rails.root.join('log', "#{Rails.env}.log"), 5, 10 * 1024 * 1024) - config.logger.formatter = ::Logger::Formatter.new - - # Enable escaping HTML in JSON. - config.active_support.escape_html_entities_in_json = true - - config.filter_parameters += %i[password credential_1 uploaded_data] + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # - - # Add additional load paths for your own custom dirs - # config.load_paths += %W( #{Rails.root}/extras ) - config.autoload_paths += %W[#{Rails.root}/app] - config.autoload_paths += %W[#{Rails.root}/lib] - - config.eager_load_paths += %W[#{Rails.root}/app] - config.eager_load_paths += %W[#{Rails.root}/lib] - - # Some lib files we don't want to autoload as they are not required in the rails app - %w[generators informatics].each { |file| Rails.autoloaders.main.ignore(Rails.root.join("lib/#{file}")) } - - # Eager load when running rake tasks. This ensures our STI classes are loaded, required for record loader - # To correctly access all purpose types - config.rake_eager_load = true - - # Load the custom inflections to help with the AASM module - Rails.autoloaders.main.inflector.inflect('aasm' => 'AASM') - - # Make Time.zone default to the specified zone, and make Active Record store time values - # in the database in UTC, and return them converted to the specified local zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Uncomment to use default local time. - config.time_zone = 'London' - - # Enable localisations to be split over multiple paths. - config.i18n.load_path = Dir[File.join(Rails.root, %w[config locales metadata *.{rb,yml}])] # rubocop:disable Rails/RootPathnameMethods - I18n.enforce_available_locales = false - - ### - # Adds image/webp to the list of content types Active Storage considers as an image - # Prevents automatic conversion to a fallback PNG, and assumes clients support WebP, - # as they support gif, jpeg, and png. - # This is possible due to broad browser support for WebP, but older browsers and - # email clients may still not support WebP. - # Requires imagemagick/libvips built with WebP support. - #++ - Rails.application.config.active_storage.web_image_content_types = %w[image/png image/jpeg image/gif] - - config.generators do |g| - g.test_framework :rspec, - fixtures: true, - view_specs: false, - helper_specs: false, - routing_specs: false, - controller_specs: false, - request_specs: true - g.fixture_replacement :factory_bot, dir: 'spec/factories' - end + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") end end diff --git a/config/boot.rb b/config/boot.rb index b81cf42113..988a5ddc46 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,38 +1,4 @@ -# frozen_string_literal: true -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) -require 'bundler/setup' # Set up gems listed in the Gemfile. - -# Bootsnap does not purge its cache, which can cause boot-times to increase -# over time. This change will purge the cache every 30 days. In development -# I saw a reduction in boot time from 44 seconds, to 17. -# In production we don't persist the cache between deployments, so wont -# see any benifit, and in practice the purge is unlikely to trigger as its rare -# we go over a month without a release. -# Disable some Rails cops, as Rails isn't actually loaded at this point. -# rubocop:disable Rails/Output, Rails/TimeZone -begin - time = File.stat('tmp/cache/bootsnap-compile-cache').birthtime - - # If our file was created more than 30 days ago. - # Note: ActiveSupport isn't loaded yet, so we can't just do 1.month.ago - # We also avoid using a constant here, as we're in the global namespace - if time < (Time.now - (60 * 60 * 24 * 30)) - print 'Purging old bootsnap cache...' - FileUtils.remove_dir('tmp/cache/bootsnap-compile-cache') - FileUtils.remove_file('tmp/cache/bootsnap-load-path-cache') - puts ' Done!' - end -rescue Errno::ENOENT, NotImplementedError - # Errno::ENOENT - # File doesn't exist. So no problems here. - # It *Might* be that the bootsnap-load-path-cache file doesn't exist - # but again, we don't actually care. - # - # NotImplementedError - # Saw this on travis where 'birthtime' was not implimented - # In this case we'll just continue. -end -# rubocop:enable Rails/Output, Rails/TimeZone - -require 'bootsnap/setup' # Speed up boot time by caching expensive operations. +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/environment.rb b/config/environment.rb index 12ea62f886..cac5315775 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,6 +1,5 @@ -# frozen_string_literal: true # Load the Rails application. -require_relative 'application' +require_relative "application" # Initialize the Rails application. Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index 060adcff1c..4cc21c4ebe 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,23 +1,13 @@ -# frozen_string_literal: true -require 'active_support/core_ext/integer/time' +require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # Configure 'rails notes' to inspect Cucumber files - config.annotations.register_directories('features') - config.annotations.register_extensions('feature') { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + # Make code changes take effect immediately without server restart. + config.enable_reloading = true - # Support requests coming from other Docker containers on localhost. - config.hosts << 'host.docker.internal' - - # In the development environment your application's code is reloaded any time - # it changes. This slows down response time but is perfect for development - # since you don't have to restart the web server when you make code changes. - config.enable_reloading = ENV.fetch('ENABLE_RELOADING', 'true') == 'true' - - # Do eager load code on boot. - config.eager_load = true + # Do not eager load code on boot. + config.eager_load = false # Show full error reports. config.consider_all_requests_local = true @@ -25,95 +15,58 @@ # Enable server timing. config.server_timing = true - # Enable/disable caching. By default caching is disabled. - # Run rails dev:cache to toggle caching. - if Rails.root.join('tmp/caching-dev.txt').exist? + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true - - config.cache_store = :memory_store - config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false - - config.cache_store = :null_store end + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false - # Disable caching for Action Mailer templates even if Action Controller - # caching is enabled. + # Make template changes take effect immediately. config.action_mailer.perform_caching = false - config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log - # Logging configuration - config.logger = ActiveSupport::Logger.new($stdout) if ENV['RAILS_LOG_TO_FILE'].blank? - config.log_level = ENV.fetch('LOG_LEVEL', :debug).to_sym - config.logger.formatter = - proc do |severity, _time, _progname, msg| - "[#{severity}] #{msg}\n" # includes non-breaking space to prevent whitespace collapse - end - - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - # Raise an error on page load if there are pending migrations. - # Disable this if we're pointing at a custom database url - custom_db = ENV.fetch('DATABASE_URL', nil).present? - config.active_record.migration_error = custom_db ? false : :page_load + config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true - # Use an evented file watcher to asynchronously detect changes in source code, - # routes, locales, etc. This feature depends on the listen gem. - use_polling_file_watcher = ENV.fetch('USE_POLLING_FILE_WATCHER', 'false') == 'true' - polling_file_watcher = ActiveSupport::FileUpdateChecker - evented_file_watcher = ActiveSupport::EventedFileUpdateChecker - config.file_watcher = use_polling_file_watcher ? polling_file_watcher : evented_file_watcher - - # Run on boot, but do not run again on reload - config.after_initialize do - Bullet.enable = ENV['WITH_BULLET'] == 'true' - Bullet.alert = ENV['NOISY_BULLET'] == 'true' - Bullet.bullet_logger = true - Bullet.rails_logger = true - end - # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true - # load WIP features flag - config.deploy_wip_pipelines = true - # Annotate rendered view with file names. config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true - # Added to enable development on pages that use
tags, as they don't pass a CSRF token. - config.action_controller.allow_forgery_protection = false - # Raise error when a before_action's only/except options reference missing actions. - config.action_controller.raise_on_missing_callback_actions = false + config.action_controller.raise_on_missing_callback_actions = true # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. # config.generators.apply_rubocop_autocorrect_after_generate! end - -Rack::MiniProfiler.config.position = 'right' diff --git a/config/environments/production.rb b/config/environments/production.rb index 9a62ac7ec1..1749607768 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,4 +1,89 @@ -# frozen_string_literal: true +require "active_support/core_ext/integer/time" -# The production configuration is set in the Deployment project and not here. -# See roles/deploy_sequencescape/templates/environment_template.rb.j2 for the actual production configuration. +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [ :request_id ] + config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) + + # Change to "debug" to log everything (including potentially personally-identifiable information!) + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + # config.cache_store = :mem_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + # config.active_job.queue_adapter = :resque + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb index 36b27692b9..c2095b1174 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,6 +1,3 @@ -# frozen_string_literal: true -require 'active_support/core_ext/integer/time' - # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped @@ -9,35 +6,24 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # Configure 'rails notes' to inspect Cucumber files - config.annotations.register_directories('features') - config.annotations.register_extensions('feature') { |tag| /#\s*(#{tag}):?\s*(.*)$/ } - # While tests run files are not watched, reloading is not necessary. config.enable_reloading = false - config.cache_classes = true - # Eager loading loads your entire application. When running a single test locally, # this is usually not necessary, and can slow down your test suite. However, it's # recommended that you enable it in continuous integration systems to ensure eager # loading is working properly before deploying your code. - config.eager_load = ENV['CI'].present? + config.eager_load = ENV["CI"].present? - # Configure public file server for tests with Cache-Control for performance. - config.public_file_server.enabled = true - config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{1.hour.to_i}" } + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } - # Show full error reports and disable caching. + # Show full error reports. config.consider_all_requests_local = true - config.action_controller.perform_caching = false config.cache_store = :null_store # Render exception templates for rescuable exceptions and raise for other exceptions. - # config.action_dispatch.show_exceptions = :rescuable - # - # Raise exceptions instead of rendering exception templates. - config.action_dispatch.show_exceptions = :none + config.action_dispatch.show_exceptions = :rescuable # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false @@ -45,28 +31,17 @@ # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test - # Disable caching for Action Mailer templates even if Action Controller - # caching is enabled. - config.action_mailer.perform_caching = false - # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test - # Unlike controllers, the mailer instance doesn't have any context about the - # incoming request so you'll need to provide the :host parameter yourself. - config.action_mailer.default_url_options = { host: 'www.example.com' } + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true @@ -74,11 +49,5 @@ # config.action_view.annotate_rendered_view_with_filenames = true # Raise error when a before_action's only/except options reference missing actions. - # config.action_controller.raise_on_missing_callback_actions = true - - # disable UI animations to avoid potential test failures - config.disable_animations = true - - # load WIP features flag - config.deploy_wip_pipelines = true + config.action_controller.raise_on_missing_callback_actions = true end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index cbb205de02..b3076b38fe 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -1,57 +1,25 @@ -# frozen_string_literal: true - # Be sure to restart your server when you modify this file. -# Define an application-wide content security policy -# For further information see the following documentation -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy - -Rails.application.config.content_security_policy do |policy| - # policy.default_src :self, :https - # policy.font_src :self, :https, :data - # policy.img_src :self, :https, :data - # policy.object_src :none - # policy.script_src :self, :https - # policy.style_src :self, :https - # Specify URI for violation reports - # policy.report_uri "/csp-violation-report-endpoint" - - # Snippet provided after running - # `bundle exec rails webpacker:install:vue` - # > You need to enable unsafe-eval rule. - # > This can be done in Rails 5.2+ for development environment in the CSP initializer - # > config/initializers/content_security_policy.rb with a snippet like this: - if Rails.env.development? - # Also allow @vite/client to hot reload javascript changes in development - policy.script_src :self, :https, :unsafe_eval, "http://#{ViteRuby.config.host_with_port}" - else - policy.script_src :self, :https - end - - # You may need to enable this in production as well depending on your setup. - policy.script_src(*policy.script_src, :blob) if Rails.env.test? - - # policy.style_src :self, :https - # Allow @vite/client to hot reload style changes in development - policy.style_src(:self, :https, :unsafe_inline) if Rails.env.development? - - # Allow @vite/client to hot reload changes in development - policy.connect_src(:self, "ws://#{ViteRuby.config.host_with_port}") if Rails.env.development? - - # # Specify URI for violation reports - # # policy.report_uri "/csp-violation-report-endpoint" -end - -# If you are using UJS then enable automatic nonce generation -Rails.application.config.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) } -Rails.application.config.content_security_policy_nonce_directives = %w[script-src] - -# Set the nonce only to specific directives -# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) - -# Report CSP violations to a specified URI -# For further information see the following documentation: -# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only -# Report only for now because we have some inline JS that can't use nonce values e.g. inline onclick event handlers -# (see ajax_handling.js for an example) -Rails.application.config.content_security_policy_report_only = true +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 5a36c5322d..c0b717f7ec 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,10 +1,8 @@ -# frozen_string_literal: true - # Be sure to restart your server when you modify this file. # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. -Rails.application.config.filter_parameters += %i[ - passw email secret token _key crypt salt certificate otp ssn +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc ] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 55a6a2b9bd..1b0cac6328 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections @@ -22,5 +20,5 @@ # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| -# inflect.acronym 'RESTful' +# inflect.acronym "RESTful" # end diff --git a/config/initializers/new_framework_defaults_8_0.rb b/config/initializers/new_framework_defaults_8_0.rb new file mode 100644 index 0000000000..92efa95152 --- /dev/null +++ b/config/initializers/new_framework_defaults_8_0.rb @@ -0,0 +1,30 @@ +# Be sure to restart your server when you modify this file. +# +# This file eases your Rails 8.0 framework defaults upgrade. +# +# Uncomment each configuration one by one to switch to the new default. +# Once your application is ready to run with all new defaults, you can remove +# this file and set the `config.load_defaults` to `8.0`. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. +# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html + +### +# Specifies whether `to_time` methods preserve the UTC offset of their receivers or preserves the timezone. +# If set to `:zone`, `to_time` methods will use the timezone of their receivers. +# If set to `:offset`, `to_time` methods will use the UTC offset. +# If `false`, `to_time` methods will convert to the local system UTC offset instead. +#++ +# Rails.application.config.active_support.to_time_preserves_timezone = :zone + +### +# When both `If-Modified-Since` and `If-None-Match` are provided by the client +# only consider `If-None-Match` as specified by RFC 7232 Section 6. +# If set to `false` both conditions need to be satisfied. +#++ +# Rails.application.config.action_dispatch.strict_freshness = true + +### +# Set `Regexp.timeout` to `1`s by default to improve security over Regexp Denial-of-Service attacks. +#++ +# Regexp.timeout = 1 diff --git a/config/puma.rb b/config/puma.rb index ce21fcba9d..a248513b24 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,15 +1,17 @@ -# frozen_string_literal: true - # This configuration file will be evaluated by Puma. The top-level methods that # are invoked here are part of Puma's configuration DSL. For more information # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. - +# # Puma starts a configurable number of processes (workers) and each process # serves each request in a thread from an internal thread pool. # +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. +# # The ideal number of threads per worker depends both on how much time the # application spends waiting for IO operations and on how much you wish to -# to prioritize throughput over latency. +# prioritize throughput over latency. # # As a rule of thumb, increasing the number of threads will increase how much # traffic a given process can handle (throughput), but due to CRuby's @@ -22,51 +24,18 @@ # Any libraries that use a connection pool or another resource pool should # be configured to provide at least as many connections as the number of # threads. This includes Active Record's `pool` parameter in `database.yml`. -threads_count = ENV.fetch('RAILS_MAX_THREADS', 3) +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. -port ENV.fetch('PORT', 3000) - -# Specifies the `environment` that Puma will run in. -environment ENV.fetch('RAILS_ENV', 'development') - -# Specifies the number of `workers` to boot in clustered mode. -# Workers are forked webserver processes. If using threads and workers together -# the concurrency of the application would be max `threads` * `workers`. -# Workers do not work on JRuby or Windows (both of which do not support -# processes). -# -# workers ENV.fetch("WEB_CONCURRENCY") { 2 } +port ENV.fetch("PORT", 3000) -# Use the `preload_app!` method when specifying a `workers` number. -# This directive tells Puma to first boot the application and load code -# before forking the application. This takes advantage of Copy On Write -# process behavior so workers use less memory. -# -# preload_app! - -# The code in the `before_worker_boot` will be called if you are using -# clustered mode by specifying a number of `workers`. After each worker -# process is booted this block will be run, if you are using `preload_app!` -# option you will want to use this block to reconnect to any threads -# or connections that may have been created at application boot, Ruby -# cannot share connections between processes. -# -# The `before_fork` and `before_worker_boot` blocks have been disabled as -# are only used when using clustered mode (i.e. workers > 1) and we are -# currently only using a single worker. - -# before_fork { Warren.handler.disconnect } - -# before_worker_boot do -# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) -# Warren.handler.connect -# end - -# Allow puma to be restarted by `rails restart` command. +# Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart +# Run the Solid Queue supervisor inside of Puma for single-server deployments +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + # Specify the PID file. Defaults to tmp/pids/server.pid in development. # In other environments, only set the PID file if requested. -pidfile ENV['PIDFILE'] if ENV['PIDFILE'] +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/public/400.html b/public/400.html new file mode 100644 index 0000000000..282dbc8cc9 --- /dev/null +++ b/public/400.html @@ -0,0 +1,114 @@ + + + + + + + The server cannot process the request due to a client error (400 Bad Request) + + + + + + + + + + + + + +
+
+ +
+
+

The server cannot process the request due to a client error. Please check the request and try again. If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000000..c0670bc877 --- /dev/null +++ b/public/404.html @@ -0,0 +1,114 @@ + + + + + + + The page you were looking for doesn’t exist (404 Not found) + + + + + + + + + + + + + +
+
+ +
+
+

The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/public/406-unsupported-browser.html b/public/406-unsupported-browser.html new file mode 100644 index 0000000000..9532a9ccd0 --- /dev/null +++ b/public/406-unsupported-browser.html @@ -0,0 +1,114 @@ + + + + + + + Your browser is not supported (406 Not Acceptable) + + + + + + + + + + + + + +
+
+ +
+
+

Your browser is not supported.
Please upgrade your browser to continue.

+
+
+ + + + diff --git a/public/422.html b/public/422.html new file mode 100644 index 0000000000..8bcf06014f --- /dev/null +++ b/public/422.html @@ -0,0 +1,114 @@ + + + + + + + The change you wanted was rejected (422 Unprocessable Entity) + + + + + + + + + + + + + +
+
+ +
+
+

The change you wanted was rejected. Maybe you tried to change something you didn’t have access to. If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000000..d77718c3a4 --- /dev/null +++ b/public/500.html @@ -0,0 +1,114 @@ + + + + + + + We’re sorry, but something went wrong (500 Internal Server Error) + + + + + + + + + + + + + +
+
+ +
+
+

We’re sorry, but something went wrong.
If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c4c9dbfbbd2f7c1421ffd5727188146213abbcef GIT binary patch literal 4166 zcmd6qU;WFw?|v@m)Sk^&NvB8tcujdV-r1b=i(NJxn&7{KTb zX$3(M+3TP2o^#KAo{#tIjl&t~(8D-k004kqPglzn0HFG(Q~(I*AKsD#M*g7!XK0T7 zN6P7j>HcT8rZgKl$v!xr806dyN19Bd4C0x_R*I-a?#zsTvb_89cyhuC&T**i|Rc zq5b8M;+{8KvoJ~uj9`u~d_f6`V&3+&ZX9x5pc8s)d175;@pjm(?dapmBcm0&vl9+W zx1ZD2o^nuyUHWj|^A8r>lUorO`wFF;>9XL-Jy!P}UXC{(z!FO%SH~8k`#|9;Q|eue zqWL0^Bp(fg_+Pkm!fDKRSY;+^@BF?AJE zCUWpXPst~hi_~u)SzYBDZroR+Z4xeHIlm_3Yc_9nZ(o_gg!jDgVa=E}Y8uDgem9`b zf=mfJ_@(BXSkW53B)F2s!&?_R4ptb1fYXlF++@vPhd=marQgEGRZS@B4g1Mu?euknL= z67P~tZ?*>-Hmi7GwlisNHHJDku-dSm7g@!=a}9cSL6Pa^w^2?&?$Oi8ibrr>w)xqx zOH_EMU@m05)9kuNR>>4@H%|){U$^yvVQ(YgOlh;5oU_-vivG-p4=LrN-k7D?*?u1u zsWly%tfAzKd6Fb=`eU2un_uaTXmcT#tlOL+aRS=kZZf}A7qT8lvcTx~7j` z*b>=z)mwg7%B2_!D0!1IZ?Nq{^Y$uI4Qx*6T!E2Col&2{k?ImCO=dD~A&9f9diXy^$x{6CwkBimn|1E09 zAMSezYtiL?O6hS37KpvDM?22&d{l)7h-!F)C-d3j8Z`c@($?mfd{R82)H>Qe`h{~G z!I}(2j(|49{LR?w4Jspl_i!(4T{31|dqCOpI52r5NhxYV+cDAu(xp*4iqZ2e-$YP= zoFOPmm|u*7C?S{Fp43y+V;>~@FFR76bCl@pTtyB93vNWy5yf;HKr8^0d7&GVIslYm zo3Tgt@M!`8B6IW&lK{Xk>%zp41G%`(DR&^u z5^pwD4>E6-w<8Kl2DzJ%a@~QDE$(e87lNhy?-Qgep!$b?5f7+&EM7$e>|WrX+=zCb z=!f5P>MxFyy;mIRxjc(H*}mceXw5a*IpC0PEYJ8Y3{JdoIW)@t97{wcUB@u+$FCCO z;s2Qe(d~oJC^`m$7DE-dsha`glrtu&v&93IZadvl_yjp!c89>zo;Krk+d&DEG4?x$ zufC1n+c1XD7dolX1q|7}uelR$`pT0Z)1jun<39$Sn2V5g&|(j~Z!wOddfYiZo7)A< z!dK`aBHOOk+-E_xbWCA3VR-+o$i5eO9`rMI#p_0xQ}rjEpGW;U!&&PKnivOcG(|m9 z!C8?WC6nCXw25WVa*eew)zQ=h45k8jSIPbq&?VE{oG%?4>9rwEeB4&qe#?-y_es4c|7ufw%+H5EY#oCgv!Lzv291#-oNlX~X+Jl5(riC~r z=0M|wMOP)Tt8@hNg&%V@Z9@J|Q#K*hE>sr6@oguas9&6^-=~$*2Gs%h#GF@h)i=Im z^iKk~ipWJg1VrvKS;_2lgs3n1zvNvxb27nGM=NXE!D4C!U`f*K2B@^^&ij9y}DTLB*FI zEnBL6y{jc?JqXWbkIZd7I16hA>(f9T!iwbIxJj~bKPfrO;>%*5nk&Lf?G@c2wvGrY&41$W{7HM9+b@&XY@>NZM5s|EK_Dp zQX60CBuantx>|d#DsaZ*8MW(we|#KTYZ=vNa#d*DJQe6hr~J6{_rI#?wi@s|&O}FR zG$kfPxheXh1?IZ{bDT-CWB4FTvO-k5scW^mi8?iY5Q`f8JcnnCxiy@m@D-%lO;y0pTLhh6i6l@x52j=#^$5_U^os}OFg zzdHbo(QI`%9#o*r8GCW~T3UdV`szO#~)^&X_(VW>o~umY9-ns9-V4lf~j z`QBD~pJ4a#b`*6bJ^3RS5y?RAgF7K5$ll97Y8#WZduZ`j?IEY~H(s^doZg>7-tk*t z4_QE1%%bb^p~4F5SB$t2i1>DBG1cIo;2(xTaj*Y~hlM{tSDHojL-QPg%Mo%6^7FrpB*{ z4G0@T{-77Por4DCMF zB_5Y~Phv%EQ64W8^GS6h?x6xh;w2{z3$rhC;m+;uD&pR74j+i22P5DS-tE8ABvH(U~indEbBUTAAAXfHZg5QpB@TgV9eI<)JrAkOI z8!TSOgfAJiWAXeM&vR4Glh;VxH}WG&V$bVb`a`g}GSpwggti*&)taV1@Ak|{WrV|5 zmNYx)Ans=S{c52qv@+jmGQ&vd6>6yX6IKq9O$3r&0xUTdZ!m1!irzn`SY+F23Rl6# zFRxws&gV-kM1NX(3(gnKpGi0Q)Dxi~#?nyzOR9!en;Ij>YJZVFAL*=R%7y%Mz9hU% zs>+ZB?qRmZ)nISx7wxY)y#cd$iaC~{k0avD>BjyF1q^mNQ1QcwsxiTySe<6C&cC6P zE`vwO9^k-d`9hZ!+r@Jnr+MF*2;2l8WjZ}DrwDUHzSF{WoG zucbSWguA!3KgB3MU%HH`R;XqVv0CcaGq?+;v_A5A2kpmk5V%qZE3yzQ7R5XWhq=eR zyUezH=@V)y>L9T-M-?tW(PQYTRBKZSVb_!$^H-Pn%ea;!vS_?M<~Tm>_rWIW43sPW z=!lY&fWc1g7+r?R)0p8(%zp&vl+FK4HRkns%BW+Up&wK8!lQ2~bja|9bD12WrKn#M zK)Yl9*8$SI7MAwSK$%)dMd>o+1UD<2&aQMhyjS5R{-vV+M;Q4bzl~Z~=4HFj_#2V9 zB)Gfzx3ncy@uzx?yzi}6>d%-?WE}h7v*w)Jr_gBl!2P&F3DX>j_1#--yjpL%<;JMR z*b70Gr)MMIBWDo~#<5F^Q0$VKI;SBIRneuR7)yVsN~A9I@gZTXe)E?iVII+X5h0~H zx^c(fP&4>!*q>fb6dAOC?MI>Cz3kld#J*;uik+Ps49cwm1B4 zZc1|ZxYyTv;{Z!?qS=D)sgRKx^1AYf%;y_V&VgZglfU>d+Ufk5&LV$sKv}Hoj+s; xK3FZRYdhbXT_@RW*ff3@`D1#ps#~H)p+y&j#(J|vk^lW{fF9OJt5(B-_&*Xgn9~3N literal 0 HcmV?d00001 diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000000..04b34bf83f --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,3 @@ + + + From 6ae093225aadf7112d6ddb1902834a0733e1dc5d Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 8 Apr 2026 14:32:11 +0100 Subject: [PATCH 060/125] style: update rubocop --- .rubocop.yml | 26 +++++++++++++++++++++++++- .rubocop_todo.yml | 26 +++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 0b54779d0b..1711d4103c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -255,6 +255,20 @@ Style/EmptyClassDefinition: Style/EmptyMethod: EnforcedStyle: expanded +# This is now default in Ruby 3.4 +# Excluding generated files +Style/FrozenStringLiteralComment: + Exclude: + - bin/* + - config/application.rb + - config/boot.rb + - config/environment.rb + - config/environments/*.rb + - config/initializers/content_security_policy.rb + - config/initializers/filter_parameter_logging.rb + - config/initializers/inflections.rb + - config/puma.rb + # Only use shorthand hash syntax when all keys match the variables for better readability Style/HashSyntax: EnforcedShorthandSyntax: consistent @@ -269,7 +283,17 @@ Style/QuotedSymbols: Exclude: - bin/* -# these are generally generated files and don't need excessive linting +# Files generated by Rails Style/StringLiterals: Exclude: - bin/* + - config/application.rb + - config/boot.rb + - config/environment.rb + - config/environments/*.rb + - config/puma.rb + +# Files generated by Rails and frequently updated +Style/SymbolArray: + Exclude: + - config/initializers/filter_parameter_logging.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6a3f7a19c3..ea0487373f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --no-exclude-limit` -# on 2026-03-13 13:25:22 UTC using RuboCop version 1.85.1. +# on 2026-04-08 13:41:45 UTC using RuboCop version 1.85.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -1602,6 +1602,30 @@ Rails/RootPathnameMethods: Exclude: - 'spec/rails_helper.rb' +# Offense count: 19 +# This cop supports unsafe autocorrection (--autocorrect-all). +Rails/StrongParametersExpect: + Exclude: + - 'app/controllers/admin/bait_libraries/bait_library_suppliers_controller.rb' + - 'app/controllers/admin/primer_panels_controller.rb' + - 'app/controllers/admin/programs_controller.rb' + - 'app/controllers/batches_controller.rb' + - 'app/controllers/bulk_submission_excel/downloads_controller.rb' + - 'app/controllers/labware_controller.rb' + - 'app/controllers/location_reports_controller.rb' + - 'app/controllers/npg_actions/assets_controller.rb' + - 'app/controllers/phi_x/spiked_buffers_controller.rb' + - 'app/controllers/phi_x/stocks_controller.rb' + - 'app/controllers/poolings_controller.rb' + - 'app/controllers/qc_files_controller.rb' + - 'app/controllers/receptacles_controller.rb' + - 'app/controllers/report_fails_controller.rb' + - 'app/controllers/stock_stampers_controller.rb' + - 'app/controllers/tag_groups_controller.rb' + - 'app/controllers/tag_layout_templates_controller.rb' + - 'app/controllers/tag_sets_controller.rb' + - 'app/controllers/tag_substitutions_controller.rb' + # Offense count: 2 Rails/ThreeStateBooleanColumn: Exclude: From f3ad4da10e839ec506e99d9acdaf0074ebe7660d Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 8 Apr 2026 15:41:37 +0100 Subject: [PATCH 061/125] fix: remove unwanted new assets --- public/400.html | 114 ---------------------------- public/404.html | 114 ---------------------------- public/406-unsupported-browser.html | 114 ---------------------------- public/422.html | 114 ---------------------------- public/500.html | 114 ---------------------------- public/icon.png | Bin 4166 -> 0 bytes public/icon.svg | 3 - 7 files changed, 573 deletions(-) delete mode 100644 public/400.html delete mode 100644 public/404.html delete mode 100644 public/406-unsupported-browser.html delete mode 100644 public/422.html delete mode 100644 public/500.html delete mode 100644 public/icon.png delete mode 100644 public/icon.svg diff --git a/public/400.html b/public/400.html deleted file mode 100644 index 282dbc8cc9..0000000000 --- a/public/400.html +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - The server cannot process the request due to a client error (400 Bad Request) - - - - - - - - - - - - - -
-
- -
-
-

The server cannot process the request due to a client error. Please check the request and try again. If you’re the application owner check the logs for more information.

-
-
- - - - diff --git a/public/404.html b/public/404.html deleted file mode 100644 index c0670bc877..0000000000 --- a/public/404.html +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - The page you were looking for doesn’t exist (404 Not found) - - - - - - - - - - - - - -
-
- -
-
-

The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.

-
-
- - - - diff --git a/public/406-unsupported-browser.html b/public/406-unsupported-browser.html deleted file mode 100644 index 9532a9ccd0..0000000000 --- a/public/406-unsupported-browser.html +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - Your browser is not supported (406 Not Acceptable) - - - - - - - - - - - - - -
-
- -
-
-

Your browser is not supported.
Please upgrade your browser to continue.

-
-
- - - - diff --git a/public/422.html b/public/422.html deleted file mode 100644 index 8bcf06014f..0000000000 --- a/public/422.html +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - The change you wanted was rejected (422 Unprocessable Entity) - - - - - - - - - - - - - -
-
- -
-
-

The change you wanted was rejected. Maybe you tried to change something you didn’t have access to. If you’re the application owner check the logs for more information.

-
-
- - - - diff --git a/public/500.html b/public/500.html deleted file mode 100644 index d77718c3a4..0000000000 --- a/public/500.html +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - We’re sorry, but something went wrong (500 Internal Server Error) - - - - - - - - - - - - - -
-
- -
-
-

We’re sorry, but something went wrong.
If you’re the application owner check the logs for more information.

-
-
- - - - diff --git a/public/icon.png b/public/icon.png deleted file mode 100644 index c4c9dbfbbd2f7c1421ffd5727188146213abbcef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4166 zcmd6qU;WFw?|v@m)Sk^&NvB8tcujdV-r1b=i(NJxn&7{KTb zX$3(M+3TP2o^#KAo{#tIjl&t~(8D-k004kqPglzn0HFG(Q~(I*AKsD#M*g7!XK0T7 zN6P7j>HcT8rZgKl$v!xr806dyN19Bd4C0x_R*I-a?#zsTvb_89cyhuC&T**i|Rc zq5b8M;+{8KvoJ~uj9`u~d_f6`V&3+&ZX9x5pc8s)d175;@pjm(?dapmBcm0&vl9+W zx1ZD2o^nuyUHWj|^A8r>lUorO`wFF;>9XL-Jy!P}UXC{(z!FO%SH~8k`#|9;Q|eue zqWL0^Bp(fg_+Pkm!fDKRSY;+^@BF?AJE zCUWpXPst~hi_~u)SzYBDZroR+Z4xeHIlm_3Yc_9nZ(o_gg!jDgVa=E}Y8uDgem9`b zf=mfJ_@(BXSkW53B)F2s!&?_R4ptb1fYXlF++@vPhd=marQgEGRZS@B4g1Mu?euknL= z67P~tZ?*>-Hmi7GwlisNHHJDku-dSm7g@!=a}9cSL6Pa^w^2?&?$Oi8ibrr>w)xqx zOH_EMU@m05)9kuNR>>4@H%|){U$^yvVQ(YgOlh;5oU_-vivG-p4=LrN-k7D?*?u1u zsWly%tfAzKd6Fb=`eU2un_uaTXmcT#tlOL+aRS=kZZf}A7qT8lvcTx~7j` z*b>=z)mwg7%B2_!D0!1IZ?Nq{^Y$uI4Qx*6T!E2Col&2{k?ImCO=dD~A&9f9diXy^$x{6CwkBimn|1E09 zAMSezYtiL?O6hS37KpvDM?22&d{l)7h-!F)C-d3j8Z`c@($?mfd{R82)H>Qe`h{~G z!I}(2j(|49{LR?w4Jspl_i!(4T{31|dqCOpI52r5NhxYV+cDAu(xp*4iqZ2e-$YP= zoFOPmm|u*7C?S{Fp43y+V;>~@FFR76bCl@pTtyB93vNWy5yf;HKr8^0d7&GVIslYm zo3Tgt@M!`8B6IW&lK{Xk>%zp41G%`(DR&^u z5^pwD4>E6-w<8Kl2DzJ%a@~QDE$(e87lNhy?-Qgep!$b?5f7+&EM7$e>|WrX+=zCb z=!f5P>MxFyy;mIRxjc(H*}mceXw5a*IpC0PEYJ8Y3{JdoIW)@t97{wcUB@u+$FCCO z;s2Qe(d~oJC^`m$7DE-dsha`glrtu&v&93IZadvl_yjp!c89>zo;Krk+d&DEG4?x$ zufC1n+c1XD7dolX1q|7}uelR$`pT0Z)1jun<39$Sn2V5g&|(j~Z!wOddfYiZo7)A< z!dK`aBHOOk+-E_xbWCA3VR-+o$i5eO9`rMI#p_0xQ}rjEpGW;U!&&PKnivOcG(|m9 z!C8?WC6nCXw25WVa*eew)zQ=h45k8jSIPbq&?VE{oG%?4>9rwEeB4&qe#?-y_es4c|7ufw%+H5EY#oCgv!Lzv291#-oNlX~X+Jl5(riC~r z=0M|wMOP)Tt8@hNg&%V@Z9@J|Q#K*hE>sr6@oguas9&6^-=~$*2Gs%h#GF@h)i=Im z^iKk~ipWJg1VrvKS;_2lgs3n1zvNvxb27nGM=NXE!D4C!U`f*K2B@^^&ij9y}DTLB*FI zEnBL6y{jc?JqXWbkIZd7I16hA>(f9T!iwbIxJj~bKPfrO;>%*5nk&Lf?G@c2wvGrY&41$W{7HM9+b@&XY@>NZM5s|EK_Dp zQX60CBuantx>|d#DsaZ*8MW(we|#KTYZ=vNa#d*DJQe6hr~J6{_rI#?wi@s|&O}FR zG$kfPxheXh1?IZ{bDT-CWB4FTvO-k5scW^mi8?iY5Q`f8JcnnCx
iy@m@D-%lO;y0pTLhh6i6l@x52j=#^$5_U^os}OFg zzdHbo(QI`%9#o*r8GCW~T3UdV`szO#~)^&X_(VW>o~umY9-ns9-V4lf~j z`QBD~pJ4a#b`*6bJ^3RS5y?RAgF7K5$ll97Y8#WZduZ`j?IEY~H(s^doZg>7-tk*t z4_QE1%%bb^p~4F5SB$t2i1>DBG1cIo;2(xTaj*Y~hlM{tSDHojL-QPg%Mo%6^7FrpB*{ z4G0@T{-77Por4DCMF zB_5Y~Phv%EQ64W8^GS6h?x6xh;w2{z3$rhC;m+;uD&pR74j+i22P5DS-tE8ABvH(U~indEbBUTAAAXfHZg5QpB@TgV9eI<)JrAkOI z8!TSOgfAJiWAXeM&vR4Glh;VxH}WG&V$bVb`a`g}GSpwggti*&)taV1@Ak|{WrV|5 zmNYx)Ans=S{c52qv@+jmGQ&vd6>6yX6IKq9O$3r&0xUTdZ!m1!irzn`SY+F23Rl6# zFRxws&gV-kM1NX(3(gnKpGi0Q)Dxi~#?nyzOR9!en;Ij>YJZVFAL*=R%7y%Mz9hU% zs>+ZB?qRmZ)nISx7wxY)y#cd$iaC~{k0avD>BjyF1q^mNQ1QcwsxiTySe<6C&cC6P zE`vwO9^k-d`9hZ!+r@Jnr+MF*2;2l8WjZ}DrwDUHzSF{WoG zucbSWguA!3KgB3MU%HH`R;XqVv0CcaGq?+;v_A5A2kpmk5V%qZE3yzQ7R5XWhq=eR zyUezH=@V)y>L9T-M-?tW(PQYTRBKZSVb_!$^H-Pn%ea;!vS_?M<~Tm>_rWIW43sPW z=!lY&fWc1g7+r?R)0p8(%zp&vl+FK4HRkns%BW+Up&wK8!lQ2~bja|9bD12WrKn#M zK)Yl9*8$SI7MAwSK$%)dMd>o+1UD<2&aQMhyjS5R{-vV+M;Q4bzl~Z~=4HFj_#2V9 zB)Gfzx3ncy@uzx?yzi}6>d%-?WE}h7v*w)Jr_gBl!2P&F3DX>j_1#--yjpL%<;JMR z*b70Gr)MMIBWDo~#<5F^Q0$VKI;SBIRneuR7)yVsN~A9I@gZTXe)E?iVII+X5h0~H zx^c(fP&4>!*q>fb6dAOC?MI>Cz3kld#J*;uik+Ps49cwm1B4 zZc1|ZxYyTv;{Z!?qS=D)sgRKx^1AYf%;y_V&VgZglfU>d+Ufk5&LV$sKv}Hoj+s; xK3FZRYdhbXT_@RW*ff3@`D1#ps#~H)p+y&j#(J|vk^lW{fF9OJt5(B-_&*Xgn9~3N diff --git a/public/icon.svg b/public/icon.svg deleted file mode 100644 index 04b34bf83f..0000000000 --- a/public/icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - From 32205474611bdd21fd23a646d3a2d45bba9d6c7a Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 8 Apr 2026 16:18:17 +0100 Subject: [PATCH 062/125] fix: revert unwanted introduced changes --- .rubocop.yml | 3 +- bin/setup | 26 +++++++- config/application.rb | 55 +++++++++++++++- config/boot.rb | 33 ++++++++++ config/environments/development.rb | 65 ++++++++++++++++--- config/environments/test.rb | 25 ++++++- .../initializers/content_security_policy.rb | 21 ++++-- config/initializers/exceptions_app.rb | 4 ++ .../initializers/filter_parameter_logging.rb | 4 ++ config/puma.rb | 14 ++++ 10 files changed, 228 insertions(+), 22 deletions(-) create mode 100644 config/initializers/exceptions_app.rb diff --git a/.rubocop.yml b/.rubocop.yml index 1711d4103c..5709cbcfd1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -26,11 +26,12 @@ plugins: AllCops: NewCops: enable Exclude: + - bin/* + - config/initializers/content_security_policy.rb - db/schema.rb - db/views_schema.rb - features/support/env.rb - lib/tasks/cucumber.rake - - bin/* - node_modules/**/* - tmp/**/* diff --git a/bin/setup b/bin/setup index be3db3c0d6..f56c791e20 100755 --- a/bin/setup +++ b/bin/setup @@ -2,6 +2,7 @@ require "fileutils" APP_ROOT = File.expand_path("..", __dir__) +# APP_NAME set in lib/deployed.rb def system!(*args) system(*args, exception: true) @@ -11,9 +12,30 @@ FileUtils.chdir APP_ROOT do # This script is a way to set up or update your development environment automatically. # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. + puts '== Checking basics' + expected_version = (/[\d.]+/.match(File.read('.ruby-version')))[0] + unless RUBY_VERSION == expected_version + puts "Expected ruby #{expected_version} got #{RUBY_VERSION}" + puts "Please install ruby #{expected_version}" + puts 'You may want to install a ruby version manager' + puts 'https://github.com/rbenv/rbenv' + end puts "== Installing dependencies ==" - system("bundle check") || system!("bundle install") + + # Set SKIP_AUTOMATIC_GEM_INSTALLATION to disable bundle install here on the CI + # suite as we've already done it, and this section gets a little clever. + unless ENV['SKIP_AUTOMATIC_GEM_INSTALLATION'] + system! 'gem install bundler --conservative' + + system('bundle check') || system!('bundle install --jobs 4 --retry 3') + end + + # Install JavaScript dependencies if using Yarn + system('bin/yarn') + + puts "\n== Setting credentials ==" + system 'bin/rails credentials:edit' # puts "\n== Copying sample files ==" # unless File.exist?("config/database.yml") @@ -21,7 +43,7 @@ FileUtils.chdir APP_ROOT do # end puts "\n== Preparing database ==" - system! "bin/rails db:prepare" + system! "bin/rails db:setup" # db:prepare breaks the views_schema loading. TODO: Y25-282 puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" diff --git a/config/application.rb b/config/application.rb index ef94e692b4..4964808cd5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,17 +11,66 @@ class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.2 + # Settings in config/environments/* take precedence over those specified here. + # Application configuration can go into files in config/initializers + # -- all .rb files in that directory are automatically loaded after loading + # the framework and any gems in your application. + + # Default options which predate the Rails 5 switch + # Due to loading order, set these here and not in an initializer, see https://github.com/rails/rails/issues/23589 + config.active_record.belongs_to_required_by_default = false + config.action_controller.forgery_protection_origin_check = false + config.action_controller.per_form_csrf_tokens = false + + config.logger = Logger.new(Rails.root.join('log', "#{Rails.env}.log"), 5, 10 * 1024 * 1024) + config.logger.formatter = ::Logger::Formatter.new + + # Enable escaping HTML in JSON. + config.active_support.escape_html_entities_in_json = true + # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. # Common ones are `templates`, `generators`, or `middleware`, for example. - config.autoload_lib(ignore: %w[assets tasks]) + config.autoload_lib(ignore: %w[assets tasks generators informatics]) # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # - # config.time_zone = "Central Time (US & Canada)" - # config.eager_load_paths << Rails.root.join("extras") + # Make Time.zone default to the specified zone, and make Active Record store time values + # in the database in UTC, and return them converted to the specified local zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Uncomment to use default local time. + config.time_zone = 'London' + + # Add additional load paths for your own custom dirs + # config.load_paths += %W( #{Rails.root}/extras ) + config.autoload_paths += %W[#{Rails.root}/app] + config.autoload_paths += %W[#{Rails.root}/lib] + + config.eager_load_paths += %W[#{Rails.root}/app] + config.eager_load_paths += %W[#{Rails.root}/lib] + + # Eager load when running rake tasks. This ensures our STI classes are loaded, required for record loader + # To correctly access all purpose types + config.rake_eager_load = true + + # Load the custom inflections to help with the AASM module + Rails.autoloaders.main.inflector.inflect('aasm' => 'AASM') + + # Enable localisations to be split over multiple paths. + config.i18n.load_path = Dir[File.join(Rails.root, %w[config locales metadata *.{rb,yml}])] # rubocop:disable Rails/RootPathnameMethods + I18n.enforce_available_locales = false + + config.generators do |g| + g.test_framework :rspec, + fixtures: true, + view_specs: false, + helper_specs: false, + routing_specs: false, + controller_specs: false, + request_specs: true + g.fixture_replacement :factory_bot, dir: 'spec/factories' + end end end diff --git a/config/boot.rb b/config/boot.rb index 988a5ddc46..3b48b7a901 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,4 +1,37 @@ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. + +# Bootsnap does not purge its cache, which can cause boot-times to increase +# over time. This change will purge the cache every 30 days. In development +# I saw a reduction in boot time from 44 seconds, to 17. +# In production we don't persist the cache between deployments, so wont +# see any benifit, and in practice the purge is unlikely to trigger as its rare +# we go over a month without a release. +# Disable some Rails cops, as Rails isn't actually loaded at this point. +# rubocop:disable Rails/Output, Rails/TimeZone +begin + time = File.stat('tmp/cache/bootsnap-compile-cache').birthtime + + # If our file was created more than 30 days ago. + # Note: ActiveSupport isn't loaded yet, so we can't just do 1.month.ago + # We also avoid using a constant here, as we're in the global namespace + if time < (Time.now - (60 * 60 * 24 * 30)) + print 'Purging old bootsnap cache...' + FileUtils.remove_dir('tmp/cache/bootsnap-compile-cache') + FileUtils.remove_file('tmp/cache/bootsnap-load-path-cache') + puts ' Done!' + end +rescue Errno::ENOENT, NotImplementedError + # Errno::ENOENT + # File doesn't exist. So no problems here. + # It *Might* be that the bootsnap-load-path-cache file doesn't exist + # but again, we don't actually care. + # + # NotImplementedError + # Saw this on travis where 'birthtime' was not implimented + # In this case we'll just continue. +end +# rubocop:enable Rails/Output, Rails/TimeZone + require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/environments/development.rb b/config/environments/development.rb index 4cc21c4ebe..05b2c161ce 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -3,11 +3,20 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # Make code changes take effect immediately without server restart. - config.enable_reloading = true + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories('features') + config.annotations.register_extensions('feature') { |tag| /#\s*(#{tag}):?\s*(.*)$/ } - # Do not eager load code on boot. - config.eager_load = false + # Support requests coming from other Docker containers on localhost. + config.hosts << 'host.docker.internal' + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.enable_reloading = ENV.fetch('ENABLE_RELOADING', 'true') == 'true' + + # Do eager load code on boot. + config.eager_load = true # Show full error reports. config.consider_all_requests_local = true @@ -20,13 +29,14 @@ if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false - end - # Change to :null_store to avoid any caching. - config.cache_store = :memory_store + config.cache_store = :null_store + end # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local @@ -43,8 +53,24 @@ # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log + # Logging configuration + config.logger = ActiveSupport::Logger.new($stdout) if ENV['RAILS_LOG_TO_FILE'].blank? + config.log_level = ENV.fetch('LOG_LEVEL', :debug).to_sym + config.logger.formatter = + proc do |severity, _time, _progname, msg| + "[#{severity}] #{msg}\n" # includes non-breaking space to prevent whitespace collapse + end + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Raise an error on page load if there are pending migrations. - config.active_record.migration_error = :page_load + # Disable this if we're pointing at a custom database url + custom_db = ENV.fetch('DATABASE_URL', nil).present? + config.active_record.migration_error = custom_db ? false : :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true @@ -55,18 +81,41 @@ # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + use_polling_file_watcher = ENV.fetch('USE_POLLING_FILE_WATCHER', 'false') == 'true' + polling_file_watcher = ActiveSupport::FileUpdateChecker + evented_file_watcher = ActiveSupport::EventedFileUpdateChecker + config.file_watcher = use_polling_file_watcher ? polling_file_watcher : evented_file_watcher + + # Run on boot, but do not run again on reload + config.after_initialize do + Bullet.enable = ENV['WITH_BULLET'] == 'true' + Bullet.alert = ENV['NOISY_BULLET'] == 'true' + Bullet.bullet_logger = true + Bullet.rails_logger = true + end + # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true + # load WIP features flag + config.deploy_wip_pipelines = true + # Annotate rendered view with file names. config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true + # Added to enable development on pages that use tags, as they don't pass a CSRF token. + config.action_controller.allow_forgery_protection = false + # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. # config.generators.apply_rubocop_autocorrect_after_generate! end + +Rack::MiniProfiler.config.position = 'right' diff --git a/config/environments/test.rb b/config/environments/test.rb index c2095b1174..17adc64030 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -6,9 +6,15 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. + # Configure 'rails notes' to inspect Cucumber files + config.annotations.register_directories('features') + config.annotations.register_extensions('feature') { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + # While tests run files are not watched, reloading is not necessary. config.enable_reloading = false + config.cache_classes = true + # Eager loading loads your entire application. When running a single test locally, # this is usually not necessary, and can slow down your test suite. However, it's # recommended that you enable it in continuous integration systems to ensure eager @@ -18,12 +24,15 @@ # Configure public file server for tests with cache-control for performance. config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } - # Show full error reports. + # Show full error reports and disable caching. config.consider_all_requests_local = true config.cache_store = :null_store # Render exception templates for rescuable exceptions and raise for other exceptions. - config.action_dispatch.show_exceptions = :rescuable + # config.action_dispatch.show_exceptions = :rescuable + # + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = :none # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false @@ -42,6 +51,12 @@ # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true @@ -50,4 +65,10 @@ # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true + + # disable UI animations to avoid potential test failures + config.disable_animations = true + + # load WIP features flag + config.deploy_wip_pipelines = true end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index b3076b38fe..cc74559532 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -4,22 +4,31 @@ # See the Securing Rails Applications Guide for more information: # https://guides.rubyonrails.org/security.html#content-security-policy-header -# Rails.application.configure do -# config.content_security_policy do |policy| +Rails.application.configure do + config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https + # Allow @vite/client to hot reload javascript changes in development + policy.script_src *policy.script_src, :unsafe_eval, "http://#{ ViteRuby.config.host_with_port }" if Rails.env.development? + + # You may need to enable this in production as well depending on your setup. + policy.script_src *policy.script_src, :blob if Rails.env.test? + # policy.style_src :self, :https + # Allow @vite/client to hot reload style changes in development + policy.style_src *policy.style_src, :unsafe_inline if Rails.env.development? + # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" -# end + end # # # Generate session nonces for permitted importmap, inline scripts, and inline styles. -# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } -# config.content_security_policy_nonce_directives = %w(script-src style-src) + config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } + config.content_security_policy_nonce_directives = %w(script-src style-src) # # # Report violations without enforcing the policy. # # config.content_security_policy_report_only = true -# end +end diff --git a/config/initializers/exceptions_app.rb b/config/initializers/exceptions_app.rb new file mode 100644 index 0000000000..53e0860dcb --- /dev/null +++ b/config/initializers/exceptions_app.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Sets the exceptions application invoked by the ShowException middleware when an exception happens. +Rails.application.config.exceptions_app = routes diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index c0b717f7ec..0520f05a65 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -6,3 +6,7 @@ Rails.application.config.filter_parameters += [ :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc ] +# Sequencescape specific parameters to filter +Rails.application.config.filter_parameters += [ + :uploaded_data +] diff --git a/config/puma.rb b/config/puma.rb index a248513b24..4895e74ea6 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -30,6 +30,20 @@ # Specifies the `port` that Puma will listen on to receive requests; default is 3000. port ENV.fetch("PORT", 3000) +# Specifies the `environment` that Puma will run in. +environment ENV.fetch('RAILS_ENV', 'development') + +# The `before_fork` and `before_worker_boot` blocks have been disabled as +# are only used when using clustered mode (i.e. workers > 1) and we are +# currently only using a single worker. + +# before_fork { Warren.handler.disconnect } + +# before_worker_boot do +# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) +# Warren.handler.connect +# end + # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart From 11303f7340ae1d5e054d865a06a3a77e5a50c610 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 8 Apr 2026 16:58:38 +0100 Subject: [PATCH 063/125] fix: update Rails defaults to 8.0 --- config/application.rb | 2 +- .../new_framework_defaults_8_0.rb | 30 ------------------- 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 config/initializers/new_framework_defaults_8_0.rb diff --git a/config/application.rb b/config/application.rb index 4964808cd5..cd353e724d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,7 +9,7 @@ module Sequencescape class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.2 + config.load_defaults 8.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/config/initializers/new_framework_defaults_8_0.rb b/config/initializers/new_framework_defaults_8_0.rb deleted file mode 100644 index 92efa95152..0000000000 --- a/config/initializers/new_framework_defaults_8_0.rb +++ /dev/null @@ -1,30 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file eases your Rails 8.0 framework defaults upgrade. -# -# Uncomment each configuration one by one to switch to the new default. -# Once your application is ready to run with all new defaults, you can remove -# this file and set the `config.load_defaults` to `8.0`. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. -# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html - -### -# Specifies whether `to_time` methods preserve the UTC offset of their receivers or preserves the timezone. -# If set to `:zone`, `to_time` methods will use the timezone of their receivers. -# If set to `:offset`, `to_time` methods will use the UTC offset. -# If `false`, `to_time` methods will convert to the local system UTC offset instead. -#++ -# Rails.application.config.active_support.to_time_preserves_timezone = :zone - -### -# When both `If-Modified-Since` and `If-None-Match` are provided by the client -# only consider `If-None-Match` as specified by RFC 7232 Section 6. -# If set to `false` both conditions need to be satisfied. -#++ -# Rails.application.config.action_dispatch.strict_freshness = true - -### -# Set `Regexp.timeout` to `1`s by default to improve security over Regexp Denial-of-Service attacks. -#++ -# Regexp.timeout = 1 From e5a2a4e14a8362895a6b66fcc9a1307520fee038 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 8 Apr 2026 17:00:13 +0100 Subject: [PATCH 064/125] fix: revert production config --- config/environments/production.rb | 91 +------------------------------ 1 file changed, 3 insertions(+), 88 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 1749607768..9a62ac7ec1 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,89 +1,4 @@ -require "active_support/core_ext/integer/time" +# frozen_string_literal: true -Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb. - - # Code is not reloaded between requests. - config.enable_reloading = false - - # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). - config.eager_load = true - - # Full error reports are disabled. - config.consider_all_requests_local = false - - # Turn on fragment caching in view templates. - config.action_controller.perform_caching = true - - # Cache assets for far-future expiry since they are all digest stamped. - config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } - - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.asset_host = "http://assets.example.com" - - # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local - - # Assume all access to the app is happening through a SSL-terminating reverse proxy. - config.assume_ssl = true - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true - - # Skip http-to-https redirect for the default health check endpoint. - # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } - - # Log to STDOUT with the current request id as a default log tag. - config.log_tags = [ :request_id ] - config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) - - # Change to "debug" to log everything (including potentially personally-identifiable information!) - config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") - - # Prevent health checks from clogging up the logs. - config.silence_healthcheck_path = "/up" - - # Don't log any deprecations. - config.active_support.report_deprecations = false - - # Replace the default in-process memory cache store with a durable alternative. - # config.cache_store = :mem_cache_store - - # Replace the default in-process and non-durable queuing backend for Active Job. - # config.active_job.queue_adapter = :resque - - # Ignore bad email addresses and do not raise email delivery errors. - # Set this to true and configure the email server for immediate delivery to raise delivery errors. - # config.action_mailer.raise_delivery_errors = false - - # Set host to be used by links generated in mailer templates. - config.action_mailer.default_url_options = { host: "example.com" } - - # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. - # config.action_mailer.smtp_settings = { - # user_name: Rails.application.credentials.dig(:smtp, :user_name), - # password: Rails.application.credentials.dig(:smtp, :password), - # address: "smtp.example.com", - # port: 587, - # authentication: :plain - # } - - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation cannot be found). - config.i18n.fallbacks = true - - # Do not dump schema after migrations. - config.active_record.dump_schema_after_migration = false - - # Only use :id for inspections in production. - config.active_record.attributes_for_inspect = [ :id ] - - # Enable DNS rebinding protection and other `Host` header attacks. - # config.hosts = [ - # "example.com", # Allow requests from example.com - # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` - # ] - # - # Skip DNS rebinding protection for the default health check endpoint. - # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } -end +# The production configuration is set in the Deployment project and not here. +# See roles/deploy_sequencescape/templates/environment_template.rb.j2 for the actual production configuration. From 21aac1b5aea97c927011f5ab9cf1d721e2887c88 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 8 Apr 2026 17:04:28 +0100 Subject: [PATCH 065/125] revert: undo exceptions app initializer --- config/application.rb | 3 +++ config/initializers/exceptions_app.rb | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 config/initializers/exceptions_app.rb diff --git a/config/application.rb b/config/application.rb index cd353e724d..2e88dd6805 100644 --- a/config/application.rb +++ b/config/application.rb @@ -22,6 +22,9 @@ class Application < Rails::Application config.action_controller.forgery_protection_origin_check = false config.action_controller.per_form_csrf_tokens = false + # Sets the exceptions application invoked by the ShowException middleware when an exception happens. + config.exceptions_app = routes + config.logger = Logger.new(Rails.root.join('log', "#{Rails.env}.log"), 5, 10 * 1024 * 1024) config.logger.formatter = ::Logger::Formatter.new diff --git a/config/initializers/exceptions_app.rb b/config/initializers/exceptions_app.rb deleted file mode 100644 index 53e0860dcb..0000000000 --- a/config/initializers/exceptions_app.rb +++ /dev/null @@ -1,4 +0,0 @@ -# frozen_string_literal: true - -# Sets the exceptions application invoked by the ShowException middleware when an exception happens. -Rails.application.config.exceptions_app = routes From b0cb72181b3e64d50b6841e3bd9b51e368ff9620 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 8 Apr 2026 20:12:53 +0100 Subject: [PATCH 066/125] tests: add --skip-server flag --- .github/workflows/cucumber_tests.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cucumber_tests.yml b/.github/workflows/cucumber_tests.yml index 03840d7e63..b5d080d043 100644 --- a/.github/workflows/cucumber_tests.yml +++ b/.github/workflows/cucumber_tests.yml @@ -72,7 +72,7 @@ jobs: env: DBPORT: ${{ job.services.mysql.ports[3306] }} run: | - bin/setup + bin/setup --skip-server bundle exec rake assets:precompile # Actually run our tests diff --git a/README.md b/README.md index 767b7fbb12..d9b814bd3e 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ It will pick up the version from the .ruby-version file automatically To automatically install the required gems, set-up default configuration files, and set up your database run: ```shell -bin/setup +bin/setup --skip-server ``` ### Manual Sequencescape setup From 47ea66c6a231959778f1ee8a422391cda6c99edc Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 8 Apr 2026 22:55:48 +0100 Subject: [PATCH 067/125] feat: add initial template from story --- config/accession/notification-template.mjml | 132 ++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 config/accession/notification-template.mjml diff --git a/config/accession/notification-template.mjml b/config/accession/notification-template.mjml new file mode 100644 index 0000000000..80527a38a9 --- /dev/null +++ b/config/accession/notification-template.mjml @@ -0,0 +1,132 @@ + + + + + + + + + Problems with Accessioning + There are validation errors in the samples provided + + .email-card { + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); + } + + + + + + + + + + + + + + ⚠️ Problems with Accessioning ⚠️ + + + + + + + There are validation errors in the samples provided for accessioning. This notification includes details of + + {{ fields | length }} + +  accessioning samples. + + + + + + {% if fields|length >= 100 %} + {% set samples = fields | map(attribute='sample-id') | list | unique | list %} + + + More than 100 samples have errored when accessioning. + + + + {% set unique_attribute_values = fields | map(attribute='attributes') | sum(start=[]) | + map(attribute='key') | list | unique | list %} + {% if unique_attribute_values | length == 0 and fields["manifest-id"] %} + Some samples in the manifest + + {{ fields["manifest-id"] }} + +  that have errored, due to incorrect values. + {% elif unique_attribute_values | length > 5 %} + Some samples in the manifest + + {{ fields[0]["manifest-id"] }} + +  that have errored, due to incorrect values for fields like + : {{ unique_attribute_values[:5] | join(", ") }} + . + {% else %} + Some samples in the manifest + + {{ fields[0]["manifest-id"] }} + +  that have errored, due to incorrect values for fields like + : {{ unique_attribute_values | join(", ") }} + . + {% endif %} + + + + {% else %} + + {% for field in fields %} + + + + Sample ID: + {{ field["sample-id"] }} + + + {% for attribute in field.attributes %} + + + {{ attribute.key }} + : + +   + {{ attribute.value }} + + {% endfor %} + + + {% endfor %} + {% endif %} + + + + + + This is an automated message. Please do not reply. + + + + + + + From 47258bf3e0f508f0a8a62cc10626f4170d2889b0 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 9 Apr 2026 00:37:15 +0100 Subject: [PATCH 068/125] fix: add initial sequencescape styling --- config/accession/notification-template.mjml | 57 +++++++++++++-------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/config/accession/notification-template.mjml b/config/accession/notification-template.mjml index 80527a38a9..daf01d8215 100644 --- a/config/accession/notification-template.mjml +++ b/config/accession/notification-template.mjml @@ -14,35 +14,45 @@ } - - + + + + + + + + + + + + + + - + - + - + - - ⚠️ Problems with Accessioning ⚠️ - + Sequencescape - + + Accessioning Failure Notification - There are validation errors in the samples provided for accessioning. This notification includes details of - - {{ fields | length }} - -  accessioning samples. + A batch of + + {{ fields | length }} + + samples have failed to accession for study + + {{ study_name }}. +
+ Please review and address the warnings below:
@@ -119,9 +129,12 @@ {% endif %} - + - + + Open a support ticket + + This is an automated message. Please do not reply. From b4ca20863b0fadb0f4202d144b2095558f32cfb7 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 9 Apr 2026 09:49:55 +0100 Subject: [PATCH 069/125] fix: remove missing callback references --- app/controllers/admin/programs_controller.rb | 2 +- app/controllers/pipelines_controller.rb | 2 +- app/controllers/projects_controller.rb | 2 +- app/controllers/sdb/sample_manifests_controller.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/admin/programs_controller.rb b/app/controllers/admin/programs_controller.rb index 99389d4e10..0075c588dc 100644 --- a/app/controllers/admin/programs_controller.rb +++ b/app/controllers/admin/programs_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Admin::ProgramsController < ApplicationController authorize_resource - before_action :discover_program, only: %i[show edit update destroy] + before_action :discover_program, only: %i[show edit update] def index @programs = Program.all diff --git a/app/controllers/pipelines_controller.rb b/app/controllers/pipelines_controller.rb index 4401972e23..7d7cf36be1 100644 --- a/app/controllers/pipelines_controller.rb +++ b/app/controllers/pipelines_controller.rb @@ -21,7 +21,7 @@ # - Release a batch (release) # - Show a batch summary (summary) class PipelinesController < ApplicationController - before_action :find_pipeline_by_id, only: %i[show activate deactivate destroy batches] + before_action :find_pipeline_by_id, only: %i[show activate deactivate batches] before_action :prepare_batch_and_pipeline, only: %i[summary finish] after_action :set_cache_disabled!, only: [:show] diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 98f865dcea..71a75677ca 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -5,7 +5,7 @@ class ProjectsController < ApplicationController # rubocop:todo Metrics/ClassLen # It should be removed wherever possible and the correct Strong Parameter options applied in its place. before_action :evil_parameter_hack! before_action :login_required - before_action :set_variables_for_project, only: %i[show edit update destroy studies] + before_action :set_variables_for_project, only: %i[show edit update destroy] # TODO: before_action :redirect_if_not_owner_or_admin, :only => [:create, :update, :destroy, :edit, :new] diff --git a/app/controllers/sdb/sample_manifests_controller.rb b/app/controllers/sdb/sample_manifests_controller.rb index 9943f2a5b5..0dc12cd373 100644 --- a/app/controllers/sdb/sample_manifests_controller.rb +++ b/app/controllers/sdb/sample_manifests_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class Sdb::SampleManifestsController < Sdb::BaseController # rubocop:todo Metrics/ClassLength - before_action :set_sample_manifest_id, only: %i[show generated print_labels] + before_action :set_sample_manifest_id, only: %i[show print_labels] before_action :validate_type, only: %i[new create] LIMIT_ERROR_LENGTH = 10_000 From a6faf6d1852043cd1aca428962a434fdd26573da Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 9 Apr 2026 09:55:46 +0100 Subject: [PATCH 070/125] fix: attempt to address some security policy violations --- config/initializers/content_security_policy.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index cc74559532..1a4c755430 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -28,7 +28,12 @@ # # Generate session nonces for permitted importmap, inline scripts, and inline styles. config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } config.content_security_policy_nonce_directives = %w(script-src style-src) -# -# # Report violations without enforcing the policy. -# # config.content_security_policy_report_only = true + + # Report CSP violations to a specified URI + # For further information see the following documentation: + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only + # Report only for now because we have some inline JS that can't use nonce values e.g. inline onclick event handlers + # (see ajax_handling.js for an example) + # Report violations without enforcing the policy. + config.content_security_policy_report_only = true end From eec5846a1c008df12573fb87b99b4f94b20db903 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 9 Apr 2026 10:30:32 +0100 Subject: [PATCH 071/125] fix: prevent uat action category being overridden by default --- app/uat_actions/uat_actions.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/uat_actions/uat_actions.rb b/app/uat_actions/uat_actions.rb index 1bffc6d0a0..f0e0fd66c2 100644 --- a/app/uat_actions/uat_actions.rb +++ b/app/uat_actions/uat_actions.rb @@ -15,6 +15,7 @@ class UatActions CATEGORY_LIST = %i[setup_and_test generating_samples auxiliary_data quality_control uncategorised].freeze class_attribute :title, :description, :category, :message + self.category = CATEGORY_LIST.last # default category, intended to be overridden by subclasses self.message = 'Completed successfully' class << self @@ -43,11 +44,6 @@ def uat_actions @uat_actions ||= {} end - # Default category should one not be provided - def category - UatActions::CATEGORY_LIST.last - end - # Returns a hash of all registered uat_actions grouped by category and sorted def grouped_and_sorted_uat_actions # raise error if any categories are not in the list From a6c1bc69f69d812a747719d1e3de5fcd444405ce Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 9 Apr 2026 10:44:29 +0100 Subject: [PATCH 072/125] test: address changed bad request error message --- spec/controllers/npg_actions/assets_controller_spec.rb | 4 +++- spec/requests/api/v2/bait_library_layouts_spec.rb | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/controllers/npg_actions/assets_controller_spec.rb b/spec/controllers/npg_actions/assets_controller_spec.rb index 31d863944c..c44459ba04 100644 --- a/spec/controllers/npg_actions/assets_controller_spec.rb +++ b/spec/controllers/npg_actions/assets_controller_spec.rb @@ -336,7 +336,9 @@ it 'renders the exception page' do regexp = Regexp.new( - ['', 'param is missing or the value is empty: qc_information', ''].join, + ['', + 'param is missing or the value is empty or invalid: qc_information', + ''].join, Regexp::MULTILINE ) expect(response).to have_http_status(:bad_request) diff --git a/spec/requests/api/v2/bait_library_layouts_spec.rb b/spec/requests/api/v2/bait_library_layouts_spec.rb index b27befdba5..265e9bdd72 100644 --- a/spec/requests/api/v2/bait_library_layouts_spec.rb +++ b/spec/requests/api/v2/bait_library_layouts_spec.rb @@ -319,7 +319,7 @@ def perform_post 'errors' => [ { 'title' => 'Missing parameter', - 'detail' => "param is missing or the value is empty: #{missing_parameter}", + 'detail' => "param is missing or the value is empty or invalid: #{missing_parameter}", 'code' => 400, 'status' => 400 } From d3e645bd47317063e24996891c9af7718ea562a6 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 9 Apr 2026 11:59:27 +0100 Subject: [PATCH 073/125] fix: add gradient background --- config/accession/notification-template.mjml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/config/accession/notification-template.mjml b/config/accession/notification-template.mjml index daf01d8215..457e026530 100644 --- a/config/accession/notification-template.mjml +++ b/config/accession/notification-template.mjml @@ -12,11 +12,15 @@ .email-card { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); } + .bg-sequencescape-gradient { + background: linear-gradient(45deg, rgba(3, 155, 229, 1) 0%, rgba(139, 195, 74, 1) 100%); + } - + + @@ -30,7 +34,7 @@ - + From df265484e856fa089db0e8c636a74f6a8a4d567b Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 9 Apr 2026 12:35:51 +0100 Subject: [PATCH 074/125] fix: adjust styling to better reflect login page --- config/accession/notification-template.mjml | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/config/accession/notification-template.mjml b/config/accession/notification-template.mjml index 457e026530..f65b09b6d5 100644 --- a/config/accession/notification-template.mjml +++ b/config/accession/notification-template.mjml @@ -9,7 +9,7 @@ Problems with Accessioning There are validation errors in the samples provided - .email-card { + .shadow { box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12); } .bg-sequencescape-gradient { @@ -26,9 +26,9 @@ - - + + @@ -36,16 +36,17 @@ - + - + + - Sequencescape + Sequencescape - + - Accessioning Failure Notification + Accessioning Failure Notification A batch of @@ -139,7 +140,13 @@ Open a support ticket - This is an automated message. Please do not reply. + Sent automatically by Sequencescape, on behalf of the +
Production Software Development + team. +
+ Please do not reply. From 58c95c5626bd356a91c912b324417882de2b0a34 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 9 Apr 2026 12:37:40 +0100 Subject: [PATCH 075/125] fix: update link to sequencing form --- config/accession/notification-template.mjml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/accession/notification-template.mjml b/config/accession/notification-template.mjml index f65b09b6d5..93538c37a4 100644 --- a/config/accession/notification-template.mjml +++ b/config/accession/notification-template.mjml @@ -136,7 +136,7 @@ - + Open a support ticket From 4ebff56ed233c1884ab4b970e1a3eb4c58138486 Mon Sep 17 00:00:00 2001 From: yoldas Date: Thu, 9 Apr 2026 15:01:57 +0100 Subject: [PATCH 076/125] 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 077/125] 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 078/125] 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 079/125] 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 From 2bd5a0b549bb6ff6292064da2f9a34d10f7c830d Mon Sep 17 00:00:00 2001 From: Wendy Yang Date: Thu, 9 Apr 2026 19:43:10 +0100 Subject: [PATCH 080/125] add tests --- .../api/v2/order_processor_spec.rb | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 spec/controllers/api/v2/order_processor_spec.rb diff --git a/spec/controllers/api/v2/order_processor_spec.rb b/spec/controllers/api/v2/order_processor_spec.rb new file mode 100644 index 0000000000..bf50fef63d --- /dev/null +++ b/spec/controllers/api/v2/order_processor_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'rails_helper' +require_dependency Rails.root.join('app/controllers/api/v2/orders_controller').to_s + +RSpec.describe Api::V2::OrderProcessor do + describe '#find_project' do + subject(:find_project) { processor.send(:find_project) } + + let(:processor) { described_class.allocate } + let(:project_uuid) { nil } + let(:params) do + ActionController::Parameters.new( + data: { + attributes: { + project_uuid: + } + } + ) + end + + before do + allow(processor).to receive(:params).and_return(params) + end + + context 'when project_uuid is not provided' do + it 'returns nil' do + expect(find_project).to be_nil + end + end + + context 'when project_uuid does not exist' do + let(:project_uuid) { 'not-a-valid-uuid' } + + it 'raises an invalid field value error' do + expect { find_project }.to raise_error(JSONAPI::Exceptions::InvalidFieldValue) + end + end + + context 'when project_uuid exists' do + let(:project) { create(:project) } + let(:project_uuid) { project.uuid } + + it 'returns the project' do + expect(find_project).to eq(project) + end + end + end +end From 315028514316a034607fd05a8dce18488c10a08d Mon Sep 17 00:00:00 2001 From: Wendy Yang Date: Thu, 9 Apr 2026 20:48:19 +0100 Subject: [PATCH 081/125] remove the default project --- .../020_lcm_triomics_new_added_submission_templates.wip.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml b/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml index ffb579ff1a..d1c4a6e313 100644 --- a/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml +++ b/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml @@ -6,7 +6,6 @@ Limber-Htp - LCM Triomics EMSeq: request_type_keys: ["limber_lcm_triomics_emseq"] product_line_name: LCM Triomics product_catalogue_name: LCM Triomics - # project_name: "UAT Project" # LCM Triomics RNASeq submission template Limber-Htp - LCM Triomics RNASeq: @@ -15,4 +14,3 @@ Limber-Htp - LCM Triomics RNASeq: request_type_keys: ["limber_lcm_triomics_rnaseq"] product_line_name: LCM Triomics product_catalogue_name: LCM Triomics - # project_name: "UAT Project" From 8418ece5a58de4879ce53792827edaa89fca7f09 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 00:00:14 +0000 Subject: [PATCH 082/125] Update sass to version 1.99.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2dd6ac2930..50cd959a87 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^14.1.0", "postcss-preset-env": "^7.8.3", - "sass": "^1.98.0", + "sass": "^1.99.0", "select2": "^4.1.0-rc.0", "sortablejs": "^1.15.7", "terser": "^5.46.1" diff --git a/yarn.lock b/yarn.lock index d526a1b99e..4b4d3ca672 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2012,10 +2012,10 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass@^1.98.0: - version "1.98.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.98.0.tgz#924ce85a3745ccaccd976262fdc1bc0c13aa8e57" - integrity sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A== +sass@^1.99.0: + version "1.99.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.99.0.tgz#ff9d1594da4886249dfaafabbeea2dea2dc74b26" + integrity sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q== dependencies: chokidar "^4.0.0" immutable "^5.1.5" From 98396e4ffc73688aa13601d3ef5fbf0cd85cb888 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:11:19 +0000 Subject: [PATCH 083/125] Update rubocop to version 1.86.1 --- Gemfile.lock | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f58ea59f20..8b9f649bdc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -131,7 +131,7 @@ GEM backports (3.25.2) base64 (0.3.0) benchmark (0.5.0) - bigdecimal (4.1.0) + bigdecimal (4.1.1) bootsnap (1.23.0) msgpack (~> 1.2) builder (3.3.0) @@ -281,9 +281,6 @@ GEM rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.19.3) - json-schema (6.2.0) - addressable (~> 2.8) - bigdecimal (>= 3.1, < 5) jsonapi-resources (0.9.0) activerecord (>= 4.1) concurrent-ruby @@ -315,8 +312,6 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.3) - mcp (0.9.2) - json-schema (>= 4.1) memoist3 (1.0.0) method_source (1.1.0) mime-types (3.7.0) @@ -365,8 +360,8 @@ GEM nokogiri (1.19.2-x86_64-linux-gnu) racc (~> 1.4) ostruct (0.6.3) - parallel (1.27.0) - parser (3.3.10.2) + parallel (2.0.1) + parser (3.3.11.1) ast (~> 2.4.1) racc pp (0.6.3) @@ -456,7 +451,7 @@ GEM logger rbtree (0.4.6) rdoc (6.3.4.1) - regexp_parser (2.11.3) + regexp_parser (2.12.0) reline (0.6.3) io-console (~> 0.5) rest-client (2.1.0) @@ -503,19 +498,18 @@ GEM rspec-support (>= 3.13.0, < 5.0.0) rspec-support (3.13.7) ruboclean (0.7.1) - rubocop (1.85.1) + rubocop (1.86.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) - mcp (~> 0.6) - parallel (~> 1.10) + parallel (>= 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) rubocop-ast (>= 1.49.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.49.0) + rubocop-ast (1.49.1) parser (>= 3.3.7.2) prism (~> 1.7) rubocop-capybara (2.22.1) From e820fd1506589937fbaeb32d855d5641d80684a0 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Fri, 10 Apr 2026 09:14:18 +0100 Subject: [PATCH 084/125] style: lint --- spec/factories/plate_factories.rb | 2 +- spec/models/cherrypick_task/control_locator_spec.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/factories/plate_factories.rb b/spec/factories/plate_factories.rb index 23f9268e58..08f820fbd0 100644 --- a/spec/factories/plate_factories.rb +++ b/spec/factories/plate_factories.rb @@ -24,7 +24,7 @@ studies_cycle { studies.cycle } # Allow us to rotate through listed studies when building out wells projects_cycle { projects.cycle } # Allow us to rotate through listed studies when building out wells well_locations { maps.where(well_order => occupied_well_index) } - occupied_well_index { (0...well_count) } + occupied_well_index { 0...well_count } end after(:build) do |plate, evaluator| diff --git a/spec/models/cherrypick_task/control_locator_spec.rb b/spec/models/cherrypick_task/control_locator_spec.rb index 575e43a1eb..4e65fbd16e 100644 --- a/spec/models/cherrypick_task/control_locator_spec.rb +++ b/spec/models/cherrypick_task/control_locator_spec.rb @@ -83,7 +83,7 @@ let(:batch_id) { 1 } let(:total_wells) { 96 } let(:num_control_wells) { 8 } - let(:wells_to_leave_free) { (0...89) } + let(:wells_to_leave_free) { 0...89 } it_behaves_like 'an invalid ControlLocator', 0 end @@ -92,7 +92,7 @@ let(:batch_id) { 1 } let(:total_wells) { 96 } let(:num_control_wells) { 0 } - let(:wells_to_leave_free) { (0...100) } + let(:wells_to_leave_free) { 0...100 } it_behaves_like 'an invalid ControlLocator', 0, 'More wells left free than available' end @@ -112,7 +112,7 @@ let(:batch_id) { batch_id } let(:total_wells) { 96 } let(:num_control_wells) { 2 } - let(:wells_to_leave_free) { (0...8) } + let(:wells_to_leave_free) { 0...8 } it_behaves_like 'a generator of valid positions', (8...96) end @@ -139,7 +139,7 @@ context 'when over a range of batches' do skip 'This analysis is not required to be run every time, so we skip it by default' - let(:range) { (1...1000) } + let(:range) { 1...1000 } let(:control_positions) do range.map do |batch_id| described_class From a0b97ea0cfe4ae641f3bff3d92705ded694c5646 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:48:24 +0000 Subject: [PATCH 085/125] Update rubocop-minitest to version 0.39.1 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8b9f649bdc..9e483c2e0d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -518,7 +518,7 @@ GEM rubocop-factory_bot (2.28.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) - rubocop-minitest (0.38.2) + rubocop-minitest (0.39.1) lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.38.0, < 2.0) From e85b16a65bebc316120c71f772b721181412cf5d Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Fri, 10 Apr 2026 13:34:50 +0100 Subject: [PATCH 086/125] feat(submission-template): adds automated column and makes automated templates invisible --- app/models/submission_template.rb | 2 +- ...add_automated_submission_template_column.rb | 18 ++++++++++++++++++ db/schema.rb | 3 ++- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20260410121704_add_automated_submission_template_column.rb diff --git a/app/models/submission_template.rb b/app/models/submission_template.rb index de6bc53fc5..de8bf1c786 100644 --- a/app/models/submission_template.rb +++ b/app/models/submission_template.rb @@ -27,7 +27,7 @@ class SubmissionTemplate < ApplicationRecord # rubocop:todo Metrics/ClassLength SUPERCEDED_BY_UNKNOWN_TEMPLATE = -2 scope :hidden, -> { order(product_line_id: :asc).where.not(superceded_by_id: LATEST_VERSION) } - scope :visible, -> { order(product_line_id: :asc).where(superceded_by_id: LATEST_VERSION) } + scope :visible, -> { order(product_line_id: :asc).where(superceded_by_id: LATEST_VERSION, automated: false) } scope :include_product_line, -> { includes(:product_line) } def self.grouped_by_product_lines diff --git a/db/migrate/20260410121704_add_automated_submission_template_column.rb b/db/migrate/20260410121704_add_automated_submission_template_column.rb new file mode 100644 index 0000000000..9a50519939 --- /dev/null +++ b/db/migrate/20260410121704_add_automated_submission_template_column.rb @@ -0,0 +1,18 @@ +class AddAutomatedSubmissionTemplateColumn < ActiveRecord::Migration[8.0] + def change + add_column :submission_templates, :automated, :boolean, default: false, null: false + + # Automated submission templates at the time of this migration + existing_automated_template_names = [ + 'Limber-Htp - Bioscan Library Prep - Automated', + 'Limber-Htp - BGE Transition - Automated', + 'Limber-Htp - Ultima PCR Free - Ultima sequencing Automated', + 'Limber - Heron LTHR - Automated', + 'Limber - Heron LTHR V2 - Automated' + ] + + SubmissionTemplate.where(name: existing_automated_template_names).each do |template| + template.update!(automated: true) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 34cce1cefe..449424eae9 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[8.0].define(version: 2026_04_10_121704) 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 @@ -1635,6 +1635,7 @@ t.integer "superceded_by_id", default: -1, null: false t.datetime "superceded_at", precision: nil t.integer "product_catalogue_id" + t.boolean "automated", default: false, null: false t.index ["name", "superceded_by_id"], name: "name_and_superceded_by_unique_idx", unique: true t.index ["product_catalogue_id"], name: "fk_submission_templates_to_product_catalogues" end From 5aa3315f03ffa086b4c575117d11faa9e4453e49 Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Fri, 10 Apr 2026 13:37:51 +0100 Subject: [PATCH 087/125] fix: adds automated flag to seed data for relevant submission templates --- .../005_bioscan_submission_templates.yml | 1 + config/default_records/submission_templates/016_bge.yml | 1 + .../019_ultima_submission_templates.yml | 1 + lib/tasks/limber.rake | 6 ++++-- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config/default_records/submission_templates/005_bioscan_submission_templates.yml b/config/default_records/submission_templates/005_bioscan_submission_templates.yml index 08234485f9..5c010597d4 100644 --- a/config/default_records/submission_templates/005_bioscan_submission_templates.yml +++ b/config/default_records/submission_templates/005_bioscan_submission_templates.yml @@ -12,6 +12,7 @@ Limber-Htp - Bioscan Lysate Prep: Limber-Htp - Bioscan Library Prep - Automated: name: "Limber-Htp - Bioscan Library Prep - Automated" submission_class_name: "AutomatedOrder" + automated: true related_records: request_type_keys: ["limber_bioscan_library_prep"] product_line_name: Bioscan diff --git a/config/default_records/submission_templates/016_bge.yml b/config/default_records/submission_templates/016_bge.yml index a417f7d879..e2bd8176df 100644 --- a/config/default_records/submission_templates/016_bge.yml +++ b/config/default_records/submission_templates/016_bge.yml @@ -16,6 +16,7 @@ Limber-Htp - BGE ISC: Limber-Htp - BGE Transition - Automated: name: "Limber-Htp - BGE Transition - Automated" submission_class_name: "AutomatedOrder" + automated: true related_records: product_line_name: Illumina-HTP product_catalogue_name: BGE diff --git a/config/default_records/submission_templates/019_ultima_submission_templates.yml b/config/default_records/submission_templates/019_ultima_submission_templates.yml index c6356c027f..53c6a698b4 100644 --- a/config/default_records/submission_templates/019_ultima_submission_templates.yml +++ b/config/default_records/submission_templates/019_ultima_submission_templates.yml @@ -9,6 +9,7 @@ Limber-Htp - Ultima PCR Free - Ultima sequencing: product_catalogue_name: GenericNoPCR Limber-Htp - Ultima PCR Free - Ultima sequencing Automated: submission_class_name: "LinearSubmission" + automated: true related_records: request_type_keys: ["ultima_sequencing"] order_role: PCR Free diff --git a/lib/tasks/limber.rake b/lib/tasks/limber.rake index c161af0d33..dcdf45058e 100644 --- a/lib/tasks/limber.rake +++ b/lib/tasks/limber.rake @@ -206,7 +206,8 @@ namespace :limber do project_id: Limber::Helper.find_project('Project Heron').id }, product_line: ProductLine.find_by!(name: 'Illumina-HTP'), - product_catalogue: ProductCatalogue.find_by!(name: 'Generic') + product_catalogue: ProductCatalogue.find_by!(name: 'Generic'), + automated: true ) end @@ -222,7 +223,8 @@ namespace :limber do project_id: Limber::Helper.find_project('Project Heron').id }, product_line: ProductLine.find_by!(name: 'Illumina-HTP'), - product_catalogue: ProductCatalogue.find_by!(name: 'Generic') + product_catalogue: ProductCatalogue.find_by!(name: 'Generic'), + automated: true ) end From a84b51e8b35757da1961ee6e48f10f69c88b3611 Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Fri, 10 Apr 2026 13:39:23 +0100 Subject: [PATCH 088/125] style: linted --- ...0260410121704_add_automated_submission_template_column.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/db/migrate/20260410121704_add_automated_submission_template_column.rb b/db/migrate/20260410121704_add_automated_submission_template_column.rb index 9a50519939..924a69e893 100644 --- a/db/migrate/20260410121704_add_automated_submission_template_column.rb +++ b/db/migrate/20260410121704_add_automated_submission_template_column.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# Adds an 'automated' column to submission templates to differentiate templates that are used for automated submissions. class AddAutomatedSubmissionTemplateColumn < ActiveRecord::Migration[8.0] def change add_column :submission_templates, :automated, :boolean, default: false, null: false @@ -11,7 +14,7 @@ def change 'Limber - Heron LTHR V2 - Automated' ] - SubmissionTemplate.where(name: existing_automated_template_names).each do |template| + SubmissionTemplate.where(name: existing_automated_template_names).find_each do |template| template.update!(automated: true) end end From c2bbb68e8449687965460f67a45eab821e35f5e3 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Fri, 10 Apr 2026 14:01:05 +0100 Subject: [PATCH 089/125] fix: add considered sample handling and better styling --- config/accession/notification-template.mjml | 148 +++++++++++--------- 1 file changed, 80 insertions(+), 68 deletions(-) diff --git a/config/accession/notification-template.mjml b/config/accession/notification-template.mjml index 93538c37a4..444cc22d54 100644 --- a/config/accession/notification-template.mjml +++ b/config/accession/notification-template.mjml @@ -25,9 +25,16 @@ + + + + + + + @@ -50,88 +57,93 @@ A batch of - {{ fields | length }} - - samples have failed to accession for study + {{ fields.samples | length }}
 samples have failed to accession for study - {{ study_name }}{{ fields.study_name }}  and manifest + + {{ fields.manifest_id }} .
- Please review and address the warnings below: + Please review and address the warnings below. - - {% if fields|length >= 100 %} - {% set samples = fields | map(attribute='sample-id') | list | unique | list %} - + + + - More than 100 samples have errored when accessioning. + + Summary: + + + + + + + + + + + + + + + +
    + + + {% set ns = namespace(reasons=[]) %} + {% for sample in fields.samples %} + {% for reason in sample.failure_groups %} + {% if reason not in ns.reasons %} + {% set ns.reasons = ns.reasons + [reason] %} + {% endif %} + {% endfor %} + {% endfor %} + + {% for reason in ns.reasons %} + {% set ns2 = namespace(count=0) %} + {% for sample in fields.samples %} + {% if reason in sample.failure_groups %} + {% set ns2.count = ns2.count + 1 %} + {% endif %} + {% endfor %} + +
  • {{ reason }}: {{ ns2.count }} samples
  • + {% endfor %} +
+
- - - {% set unique_attribute_values = fields | map(attribute='attributes') | sum(start=[]) | - map(attribute='key') | list | unique | list %} - {% if unique_attribute_values | length == 0 and fields["manifest-id"] %} - Some samples in the manifest - - {{ fields["manifest-id"] }} - -  that have errored, due to incorrect values. - {% elif unique_attribute_values | length > 5 %} - Some samples in the manifest - - {{ fields[0]["manifest-id"] }} - -  that have errored, due to incorrect values for fields like - : {{ unique_attribute_values[:5] | join(", ") }} - . - {% else %} - Some samples in the manifest - - {{ fields[0]["manifest-id"] }} - -  that have errored, due to incorrect values for fields like - : {{ unique_attribute_values | join(", ") }} - . - {% endif %} +
+ + + + + + First  + {{ [fields.samples | length, 5] | min }} +  failures: - {% else %} - - {% for field in fields %} - - - - Sample ID: - {{ field["sample-id"] }} + {% for sample in fields.samples[:5] %} + + + + + {{ sample.sample_name }} ({{ sample.supplier_sample_name }}) + - {% for attribute in field.attributes %} - - - {{ attribute.key }} - : - -   - {{ attribute.value }} + + {{ sample.status_message }} - {% endfor %} {% endfor %} - {% endif %} @@ -141,12 +153,12 @@ Sent automatically by Sequencescape, on behalf of the - Production Software Development + + Production Software Development + team.
- Please do not reply. + Please do not reply to this email - open a support ticket if you would like further assistance.
From dabb292eb2fa3be3ffd9f7aa2770f51c4a58d426 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Fri, 10 Apr 2026 15:16:15 +0100 Subject: [PATCH 090/125] build: ignore generated templates --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d42ad8f7d2..0a27df8188 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,9 @@ app/sample_manifest_excel/doc/ docs/accessioning/**/*.png docs/accessioning/**/*.svg +# Generated files from notification templates +config/accession/*.j2 + # Test files capybara*.html spec/examples.txt From 8c287e61560636aa33b6af05d8ea9a0e7b5a6b3c Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Fri, 10 Apr 2026 15:28:32 +0100 Subject: [PATCH 091/125] tests(submission-template): adds rspec tests for submission template model --- app/models/submission_template.rb | 4 +- spec/models/submission_template_spec.rb | 341 ++++++++++++++++++++++++ 2 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 spec/models/submission_template_spec.rb diff --git a/app/models/submission_template.rb b/app/models/submission_template.rb index de8bf1c786..618385dad5 100644 --- a/app/models/submission_template.rb +++ b/app/models/submission_template.rb @@ -35,7 +35,7 @@ def self.grouped_by_product_lines end def visible - superceded_by_id == LATEST_VERSION + superceded_by_id == LATEST_VERSION && !automated end def superceded_by_unknown! @@ -89,7 +89,7 @@ def input_field_infos end def sequencing? - request_types.any?(&:sequencing) + request_types.any?(&:sequencing?) end def input_asset_type diff --git a/spec/models/submission_template_spec.rb b/spec/models/submission_template_spec.rb new file mode 100644 index 0000000000..fef52cfa22 --- /dev/null +++ b/spec/models/submission_template_spec.rb @@ -0,0 +1,341 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SubmissionTemplate do + describe 'validations' do + it 'is valid with valid attributes' do + submission_template = build(:submission_template) + expect(submission_template).to be_valid + end + + context 'without a name' do + let(:submission_template) { build(:submission_template, name: nil) } + + before { submission_template.validate } + + it 'is not valid' do + expect(submission_template).not_to be_valid + end + + it 'has an error on name' do + expect(submission_template.errors[:name]).to include("can't be blank") + end + end + + context 'without a submission_class_name' do + let(:submission_template) { build(:submission_template, submission_class_name: nil) } + + before { submission_template.validate } + + it 'is not valid' do + expect(submission_template).not_to be_valid + end + + it 'has an error on submission_class_name' do + expect(submission_template.errors[:submission_class_name]).to include("can't be blank") + end + end + + context 'without a product_catalogue' do + let(:submission_template) { build(:submission_template, product_catalogue: nil) } + + before { submission_template.validate } + + it 'is not valid' do + expect(submission_template).not_to be_valid + end + + it 'has an error on product_catalogue' do + expect(submission_template.errors[:product_catalogue]).to include("can't be blank") + end + end + end + + describe 'scopes' do + describe '.visible' do + it 'includes templates that are not superceded and not automated' do + visible_template = create(:submission_template, superceded_by_id: SubmissionTemplate::LATEST_VERSION, + automated: false) + # Automated template + create(:submission_template, superceded_by_id: SubmissionTemplate::LATEST_VERSION, automated: true) + # Superceded templates + create(:submission_template, superceded_by_id: 1, automated: false) + # Superceded and automated template + create(:submission_template, superceded_by_id: 1, automated: true) + + expect(described_class.visible).to eq([visible_template]) + end + end + + describe '.hidden' do + it 'includes templates that are superceded' do + # Superceded templates + hidden_template1 = create(:submission_template, superceded_by_id: 1, automated: false) + hidden_template2 = create(:submission_template, superceded_by_id: 1, automated: true) + # Not superceded template + create(:submission_template, superceded_by_id: SubmissionTemplate::LATEST_VERSION, automated: false) + + expect(described_class.hidden).to eq([hidden_template1, hidden_template2]) + end + end + + describe '.include_product_line' do + it 'includes associated product line' do + product_line = create(:product_line) + create(:submission_template, product_line:) + + expect(described_class.include_product_line.first.product_line).to eq(product_line) + end + end + end + + describe '#visible' do + let(:submission_template) { described_class.new(name: 'test submission') } + + context 'when superceded_by_id is LATEST_VERSION and automated is false' do + it 'returns true' do + submission_template.superceded_by_id = SubmissionTemplate::LATEST_VERSION + submission_template.automated = false + expect(submission_template.visible).to be true + end + end + + context 'when superceded_by_id is LATEST_VERSION and automated is true' do + it 'returns false' do + submission_template.superceded_by_id = SubmissionTemplate::LATEST_VERSION + submission_template.automated = true + expect(submission_template.visible).to be false + end + end + + context 'when superceded_by_id is not LATEST_VERSION and automated is false' do + it 'returns false' do + submission_template.superceded_by_id = 1 + submission_template.automated = false + expect(submission_template.visible).to be false + end + end + + context 'when superceded_by_id is not LATEST_VERSION and automated is true' do + it 'returns false' do + submission_template.superceded_by_id = 1 + submission_template.automated = true + expect(submission_template.visible).to be false + end + end + end + + describe '#superceded_by_unknown!' do + it 'sets superceded_by_id to SUPERCEDED_BY_UNKNOWN_TEMPLATE' do + submission_template = create(:submission_template) + submission_template.superceded_by_unknown! + expect(submission_template.superceded_by_id).to eq(SubmissionTemplate::SUPERCEDED_BY_UNKNOWN_TEMPLATE) + end + end + + describe '#supercede' do + let(:original_template) { create(:submission_template, name: 'Original Template') } + let(:cloned_template) { described_class.find_by(name: 'Cloned Template') } + + before do + original_template.supercede do |cloned| + cloned.name = 'Cloned Template' + end + end + + it 'creates a new submission template' do + expect(described_class.count).to eq(2) + end + + it 'sets cloned template superceded_by_id to LATEST_VERSION' do + expect(cloned_template.superceded_by_id).to eq(SubmissionTemplate::LATEST_VERSION) + end + + it 'updates original template attributes' do + expect(original_template).to have_attributes( + superceded_by_id: cloned_template.id, + superceded_at: be_present + ) + end + end + + describe '#create_order!' do + subject(:order) { submission_template.create_order!(order_attributes) } + + let!(:request_types) { create_list(:request_type, 2) } + let!(:submission_template) do + create(:submission_template, submission_parameters: { + request_type_ids_list: request_types.map(&:id) + }) + end + let!(:user) { create(:user) } + let!(:study) { create(:study) } + let!(:project) { create(:project) } + let(:order_attributes) { { user:, study:, project: } } + + it 'creates a persisted order' do + expect(order).to be_persisted + end + + it 'sets the order attributes' do + expect(order).to have_attributes( + user:, + study:, + project: + ) + end + + context 'with a block' do + subject(:order_with_block) do + submission_template.create_order!(order_attributes) do |created_order| + created_order.study = block_study + end + end + + let(:block_study) { create(:study) } + + it 'returns a persisted order' do + expect(order_with_block).to be_persisted + end + + it 'allows the block to modify the order' do + expect(order_with_block.study).to eq(block_study) + end + end + end + + describe '#create_with_submission!' do + subject(:order) { submission_template.create_with_submission!(order_attributes) } + + let!(:request_types) { create_list(:request_type, 2, asset_type: 'Well') } + let!(:submission_template) do + create(:submission_template, submission_parameters: { + request_type_ids_list: request_types.map(&:id) + }) + end + let!(:user) { create(:user) } + let!(:study) { create(:study) } + let!(:project) { create(:project) } + let!(:plate) { create(:plate, well_count: 5) } + let(:order_attributes) do + { + user: user, + study: study, + project: project, + assets: plate.wells + } + end + + it 'creates a persisted order' do + expect(order).to be_persisted + end + + it 'sets the order attributes' do + expect(order).to have_attributes( + user: user, + study: study, + project: project, + assets: plate.wells + ) + end + + it 'creates an associated submission' do + expect(order.submission).to be_present + end + + it 'sets the submission user' do + expect(order.submission.user).to eq(user) + end + end + + describe '#new_order' do + subject(:order) { submission_template.new_order(order_attributes) } + + let!(:request_types) { create_list(:request_type, 2) } + let!(:submission_template) do + create(:submission_template, submission_parameters: { + request_type_ids_list: request_types.map(&:id) + }) + end + let(:order_attributes) { { user_id: 1, study_id: 1, project_id: 1 } } + + it 'returns an order of the submission class type' do + expect(order).to be_a(submission_template.submission_class) + end + + it 'sets the order attributes' do + expect(order).to have_attributes( + user_id: 1, + study_id: 1, + project_id: 1, + template_name: submission_template.name + ) + end + end + + describe '#submission_class' do + it 'returns the class specified by submission_class_name' do + submission_template = create(:submission_template, submission_class_name: 'LinearSubmission') + expect(submission_template.submission_class).to eq(LinearSubmission) + end + end + + describe '#sequencing?' do + it 'returns true if any request type is a sequencing request' do + sequencing_request_type = create(:sequencing_request_type) + non_sequencing_request_type = create(:request_type) + submission_template = create(:submission_template, submission_parameters: { + request_type_ids_list: [sequencing_request_type.id, non_sequencing_request_type.id] + }) + + expect(submission_template.sequencing?).to be true + end + + it 'returns false if no request types have sequencing set to true' do + request_types = create_list(:request_type, 2) + submission_template = create(:submission_template, submission_parameters: { + request_type_ids_list: request_types.map(&:id) + }) + + expect(submission_template.sequencing?).to be false + end + end + + describe '#input_asset_type' do + it 'returns the asset type of the first request type in the list' do + request_type1 = create(:request_type, asset_type: 'Plate') + request_type2 = create(:request_type, asset_type: 'Tube') + submission_template = create(:submission_template, submission_parameters: { + request_type_ids_list: [request_type1.id, request_type2.id] + }) + + expect(submission_template.input_asset_type).to eq('Plate') + end + end + + describe '#input_plate_purposes' do + it 'returns the acceptable purposes of the first request type in the list' do + purposes = create_list(:purpose, 2) + request_type1 = create(:request_type, asset_type: 'Plate', acceptable_purposes: purposes) + request_type2 = create(:request_type, asset_type: 'Tube') + submission_template = create(:submission_template, submission_parameters: { + request_type_ids_list: [request_type1.id, request_type2.id] + }) + + expect(submission_template.input_plate_purposes).to eq(purposes) + end + end + + describe '#request_type_keys' do + it 'returns the keys of the request types in the list' do + request_type1 = create(:request_type, key: 'request_type_1') + request_type2 = create(:request_type, key: 'request_type_2') + submission_template = create(:submission_template, submission_parameters: { + request_type_ids_list: [request_type1.id, request_type2.id] + }) + + expect(submission_template.request_type_keys).to eq(%w[request_type_1 request_type_2]) + end + end +end From b5a8178a5b92e7365aae048c17f22c67e99320ce Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Fri, 10 Apr 2026 15:29:31 +0100 Subject: [PATCH 092/125] build: tell prettier to ignore file --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 430d204f7f..0848f4ad50 100644 --- a/.prettierignore +++ b/.prettierignore @@ -24,6 +24,7 @@ /yarn-error.log app/sample_manifest_excel/doc/ capybara*.html +config/accession/*.j2 config/cucumber.yml config/database.yml coverage From 62ae5ba81e59c3309c00f7b1c8634a910f106662 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Fri, 10 Apr 2026 15:42:22 +0100 Subject: [PATCH 093/125] fix: add colour-schemes for production and test/uat notifications --- config/accession/notification-template.mjml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/accession/notification-template.mjml b/config/accession/notification-template.mjml index 444cc22d54..ec23193759 100644 --- a/config/accession/notification-template.mjml +++ b/config/accession/notification-template.mjml @@ -19,7 +19,8 @@ - + + @@ -45,7 +46,8 @@ - + + Sequencescape From 7baa11567710fc115805beab794e6dcca42bb6a7 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Fri, 10 Apr 2026 15:46:35 +0100 Subject: [PATCH 094/125] build: tell prettier to ignore correct file --- .prettierignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.prettierignore b/.prettierignore index 0848f4ad50..f1908d47d1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,6 +13,7 @@ *.log *.log.[0-9] *.min.js +*.mjml *.orig *.sublime-workspace *.swp @@ -24,7 +25,6 @@ /yarn-error.log app/sample_manifest_excel/doc/ capybara*.html -config/accession/*.j2 config/cucumber.yml config/database.yml coverage From e009834ff9a4a8cc32bc5d8e0dec1a5ce624b7fa Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Mon, 13 Apr 2026 10:45:25 +0100 Subject: [PATCH 095/125] tests: adds test for fallback values in include_product_line --- spec/models/submission_template_spec.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/models/submission_template_spec.rb b/spec/models/submission_template_spec.rb index fef52cfa22..1eb7642228 100644 --- a/spec/models/submission_template_spec.rb +++ b/spec/models/submission_template_spec.rb @@ -87,6 +87,12 @@ expect(described_class.include_product_line.first.product_line).to eq(product_line) end + + it "returns 'general' for templates without a product line" do + create(:submission_template, product_line: nil) + + expect(described_class.grouped_by_product_lines.keys).to include('General') + end end end From 6011b75922fcf60a6afaeca0dd049ac5fdfeb6f3 Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Mon, 13 Apr 2026 10:53:46 +0100 Subject: [PATCH 096/125] tests: adds grouped_by_product_lines tests --- spec/models/submission_template_spec.rb | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/spec/models/submission_template_spec.rb b/spec/models/submission_template_spec.rb index 1eb7642228..b1b2945a93 100644 --- a/spec/models/submission_template_spec.rb +++ b/spec/models/submission_template_spec.rb @@ -87,12 +87,21 @@ expect(described_class.include_product_line.first.product_line).to eq(product_line) end + end + end - it "returns 'general' for templates without a product line" do - create(:submission_template, product_line: nil) + describe '#grouped_by_product_lines' do + it 'groups visible templates by product line name' do + product_line = create(:product_line) + submission_list = create_list(:submission_template, 5, product_line:) - expect(described_class.grouped_by_product_lines.keys).to include('General') - end + expect(described_class.grouped_by_product_lines).to eq({ product_line.name => submission_list }) + end + + it 'groups templates without a product line under "General"' do + submission_list = create_list(:submission_template, 5, product_line: nil) + + expect(described_class.grouped_by_product_lines).to eq({ 'General' => submission_list }) end end From 2d2137b204d264f804e0ed7a2176ebcdbe2bedc2 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 13 Apr 2026 13:26:21 +0100 Subject: [PATCH 097/125] remove UPF2 Balanced Pool and UPF2 Balanced Norm from sequencescape it is defined in Limber --- .../tube_purposes/011_ultima_ug200_tube_purposes.wip.yml | 8 -------- 1 file changed, 8 deletions(-) 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 index dd484cbd91..3cb41b6b30 100644 --- 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 @@ -4,11 +4,3 @@ UPF2 EqVol Norm: 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 b5e7c9cfe810b0ab7cc6f7647e98342c4e0602a3 Mon Sep 17 00:00:00 2001 From: yoldas Date: Mon, 13 Apr 2026 13:45:31 +0100 Subject: [PATCH 098/125] add UG200 to the submission template names --- .../021_ultima_ug200_submission_templates.wip.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 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 29f30ede36..5c534184d0 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 @@ -1,7 +1,7 @@ # 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: +Limber-Htp - Ultima UG200 PCR Free - Ultima UG200 sequencing: submission_class_name: "LinearSubmission" related_records: request_type_keys: @@ -9,7 +9,7 @@ Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing: order_role: PCR Free product_line_name: Ultima # same as UG100 product_catalogue_name: GenericNoPCR -Limber-Htp - Ultima PCR Free - Ultima UG200 sequencing Automated: +Limber-Htp - Ultima UG200 PCR Free - Ultima UG200 sequencing Automated: submission_class_name: "LinearSubmission" related_records: request_type_keys: ["ultima_ug200_sequencing"] From 77180d4af54789836332e1235ebb2a849c4f2f3d Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 14 Apr 2026 02:04:44 +0100 Subject: [PATCH 099/125] fix(ug200): add menu option for downloading sample sheet --- .../presenters/batch_submenu_presenter.rb | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/app/models/presenters/batch_submenu_presenter.rb b/app/models/presenters/batch_submenu_presenter.rb index ab61700e1b..ba60e58133 100644 --- a/app/models/presenters/batch_submenu_presenter.rb +++ b/app/models/presenters/batch_submenu_presenter.rb @@ -71,13 +71,35 @@ def build_submenu only_path: true } end add_submenu_option 'NPG run data', "#{configatron.run_data_by_batch_id_url}#{@batch.id}" - return unless aviti_run_manifest? || ultima_run_manifest? + return unless run_manifest? add_submenu_option 'Download Sample Sheet', id: @batch.id, controller: :batches, action: :generate_sample_sheet end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # Determines whether to display the submenu option for downloading the sample sheet. + # + # @return [Boolean] true if the link for the download should be displayed + def run_manifest? + aviti_run_manifest? || ultima_run_manifest? || ultima_ug200_run_manifest? + end + + # Determines whether the batch is released and contains any Ultima UG200 + # sequencing requests. + # + # @return [Boolean] true if the batch is released and has Ultima UG200 requests + def ultima_ug200_run_manifest? + @batch.released? && ultima_ug200? + end + + # Determines whether the batch is an Ultima UG200 batch. + # + # @return [Boolean] true if the batch's pipeline is Ultima UG200 + def ultima_ug200? + @pipeline.instance_of?(UltimaUG200SequencingPipeline) + end + # This is used to determine if we need to display the Aviti run manifest option # in the batch submenu. # @return [Boolean] true if the batch is released and has Element Aviti requests @@ -120,8 +142,11 @@ def plate_labels? cherrypicking? end + # Determines whether the batch is an Ultima sequencing batch + # @return [Boolean] true if the batch's pipeline is Ultima def ultima? - @pipeline.is_a?(UltimaSequencingPipeline) + # Use instance_of? instead of is_a? to avoid picking up subclasses. + @pipeline.instance_of?(UltimaSequencingPipeline) end end end From 7028be2675a04151acb02e1d0c247cb0dc5e49eb Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 14 Apr 2026 02:04:56 +0100 Subject: [PATCH 100/125] test(ug200): add test for menu option for downloading sample sheet --- .../presenters/batch_submenu_presenter_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/models/presenters/batch_submenu_presenter_spec.rb b/spec/models/presenters/batch_submenu_presenter_spec.rb index 7388891a0b..8f9638541f 100644 --- a/spec/models/presenters/batch_submenu_presenter_spec.rb +++ b/spec/models/presenters/batch_submenu_presenter_spec.rb @@ -78,4 +78,16 @@ end end end + + context 'when we are in the Ultima UG200 sequencing pipeline' do + let(:current_user) { create(:user) } + let(:batch) { create(:ultima_ug200_sequencing_batch, state: 'released') } + let(:generate_sample_sheet_option) do + { label: 'Download Sample Sheet', url: "/batches/#{batch.id}/generate_sample_sheet" } + end + + it 'includes a link to download the sample sheet' do + expect(batch_submenu_presenter.each_option.to_a).to include(generate_sample_sheet_option) + end + end end From f5d068c29ba292c964a87f30a1b0f07928b6caf9 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 14 Apr 2026 10:41:46 +0100 Subject: [PATCH 101/125] feat(descriptors): add feature flag to enable validation of workflow parameters based on required field --- config/feature_flags.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/feature_flags.yml b/config/feature_flags.yml index 0132f40714..97296c97d5 100644 --- a/config/feature_flags.yml +++ b/config/feature_flags.yml @@ -14,3 +14,4 @@ y25_706_enable_accessioning: Enables the accessioning feature in the application y25_714_skip_accessioning_tag_validation: Skips internal validation of accessioning tags prior to sample accessioning y25_705_notify_on_internal_accessioning_validation_failures: Sends email to developers when internal validation fails during accessioning y25_705_notify_on_external_accessioning_validation_failures: Sends email to developers when external validation fails during accessioning +y25_105_validate_descriptor_required_field: Enables validation of batch workflow task descriptor required field From edd0ec70eccd3b8850c89b99c36014a1ec382f0d Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 14 Apr 2026 10:43:55 +0100 Subject: [PATCH 102/125] feat(descriptors): add feature flag for validation of required submitted values --- app/models/descriptor.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/descriptor.rb b/app/models/descriptor.rb index 7dc9e9d447..d1c37f07f9 100644 --- a/app/models/descriptor.rb +++ b/app/models/descriptor.rb @@ -17,9 +17,11 @@ def matches?(search) # Returns an array of validation errors for the submitted descriptor value. # The value comes from the Task Details form for a workflow task on a batch. - # @return [Array] An array of error messages, empty if the value is valid + # @return [Array] An array of error messages, empty if the value is valid def validate_value(submitted_value) - return ["#{name} is required"] if submitted_value.blank? && is_required? + if is_required? && submitted_value.blank? && Flipper.enabled?(:y25_105_validate_descriptor_required_field) + return ["#{name} is required"] + end return [] if submitted_value.blank? return validate_date_value(submitted_value) if kind == 'Date' @@ -30,7 +32,7 @@ def validate_value(submitted_value) # Validates that the submitted value is a valid date string in the format # YYYY-MM-DD, and that the year is within a reasonable range. - # @return [Array] An array of error messages, empty if the value is valid + # @return [Array] An array of error messages, empty if the value is valid def validate_date_value(submitted_value) unless submitted_value.match?(/\A\d{4}-\d{2}-\d{2}\z/) return ["'#{submitted_value}' is not a valid date for #{name} (expected YYYY-MM-DD)"] From f18e537a52043589c90d0a4444c2327da611ea54 Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 14 Apr 2026 10:45:54 +0100 Subject: [PATCH 103/125] test(descriptors): add test for feature flag for validation of required submitted values --- spec/models/descriptor_spec.rb | 41 +++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/spec/models/descriptor_spec.rb b/spec/models/descriptor_spec.rb index 296041e233..2f444c52ef 100644 --- a/spec/models/descriptor_spec.rb +++ b/spec/models/descriptor_spec.rb @@ -6,9 +6,16 @@ describe '#validate_value' do subject(:errors) { descriptor.validate_value(value) } + let(:feature) { :y25_105_validate_descriptor_required_field } + + before do + # By default, disable the feature flag + allow(Flipper).to receive(:enabled?).with(feature).and_return(false) + end + context 'when kind is Date' do context 'when required is true' do - let(:descriptor) { described_class.new(name: 'OTR carrier expiry', kind: 'Date', required: true) } + let(:descriptor) { described_class.new(name: 'Some expiry', kind: 'Date', required: true) } context 'with a valid ISO 8601 date' do let(:value) { '2026-06-01' } @@ -19,7 +26,21 @@ context 'with a blank value' do let(:value) { '' } - it { is_expected.to contain_exactly('OTR carrier expiry is required') } + context 'when the feature flag is enabled' do + before do + allow(Flipper).to receive(:enabled?).with(feature).and_return(true) + end + + it { is_expected.to contain_exactly('Some expiry is required') } + end + + context 'when the feature flag is disabled' do + before do + allow(Flipper).to receive(:enabled?).with(feature).and_return(false) + end + + it { is_expected.to be_empty } + end end context 'with an invalid date string' do @@ -27,7 +48,7 @@ it { is_expected.to contain_exactly( - "'not-a-date' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" + "'not-a-date' is not a valid date for Some expiry (expected YYYY-MM-DD)" ) } end @@ -37,7 +58,7 @@ it { is_expected.to contain_exactly( - "'01/06/2026' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" + "'01/06/2026' is not a valid date for Some expiry (expected YYYY-MM-DD)" ) } end @@ -47,7 +68,7 @@ it { is_expected.to contain_exactly( - 'Date year for OTR carrier expiry must be between 1990 and 2100 (got 1989)' + 'Date year for Some expiry must be between 1990 and 2100 (got 1989)' ) } end @@ -57,7 +78,7 @@ it { is_expected.to contain_exactly( - 'Date year for OTR carrier expiry must be between 1990 and 2100 (got 2101)' + 'Date year for Some expiry must be between 1990 and 2100 (got 2101)' ) } end @@ -67,14 +88,14 @@ it { is_expected.to contain_exactly( - "'62026-06-01' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" + "'62026-06-01' is not a valid date for Some expiry (expected YYYY-MM-DD)" ) } end end context 'when required is false' do - let(:descriptor) { described_class.new(name: 'OTR carrier expiry', kind: 'Date', required: false) } + let(:descriptor) { described_class.new(name: 'Some expiry', kind: 'Date', required: false) } context 'with a blank value' do let(:value) { '' } @@ -93,7 +114,7 @@ it { is_expected.to contain_exactly( - "'not-a-date' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" + "'not-a-date' is not a valid date for Some expiry (expected YYYY-MM-DD)" ) } end @@ -103,7 +124,7 @@ it { is_expected.to contain_exactly( - "'62026-06-01' is not a valid date for OTR carrier expiry (expected YYYY-MM-DD)" + "'62026-06-01' is not a valid date for Some expiry (expected YYYY-MM-DD)" ) } end From 86a3ce9e972b442972cbdf2c6b3485286731cb5b Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 14 Apr 2026 11:22:40 +0100 Subject: [PATCH 104/125] test(descriptors): rename feature to feature_flag to prevent potential overlap with feature test --- spec/models/descriptor_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/models/descriptor_spec.rb b/spec/models/descriptor_spec.rb index 2f444c52ef..c327c16f91 100644 --- a/spec/models/descriptor_spec.rb +++ b/spec/models/descriptor_spec.rb @@ -6,11 +6,11 @@ describe '#validate_value' do subject(:errors) { descriptor.validate_value(value) } - let(:feature) { :y25_105_validate_descriptor_required_field } + let(:feature_flag) { :y25_105_validate_descriptor_required_field } before do # By default, disable the feature flag - allow(Flipper).to receive(:enabled?).with(feature).and_return(false) + allow(Flipper).to receive(:enabled?).with(feature_flag).and_return(false) end context 'when kind is Date' do @@ -28,7 +28,7 @@ context 'when the feature flag is enabled' do before do - allow(Flipper).to receive(:enabled?).with(feature).and_return(true) + allow(Flipper).to receive(:enabled?).with(feature_flag).and_return(true) end it { is_expected.to contain_exactly('Some expiry is required') } @@ -36,7 +36,7 @@ context 'when the feature flag is disabled' do before do - allow(Flipper).to receive(:enabled?).with(feature).and_return(false) + allow(Flipper).to receive(:enabled?).with(feature_flag).and_return(false) end it { is_expected.to be_empty } From d285271ad92bcef6ed875638ee52ef9f7c43051b Mon Sep 17 00:00:00 2001 From: yoldas Date: Tue, 14 Apr 2026 11:25:21 +0100 Subject: [PATCH 105/125] refactor(descriptors): move feature flag y25_105_validate_descriptor_required_field out of accessioning flags --- config/feature_flags.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/feature_flags.yml b/config/feature_flags.yml index 97296c97d5..f03307dba8 100644 --- a/config/feature_flags.yml +++ b/config/feature_flags.yml @@ -9,9 +9,10 @@ y24_052_enable_data_release_timing_validation: Enables server-side validation th y25_442_make_api_key_mandatory: Makes API key mandatory for all API requests +y25_105_validate_descriptor_required_field: Enables validation of batch workflow task descriptor required field + # Accessioning y25_706_enable_accessioning: Enables the accessioning feature in the application y25_714_skip_accessioning_tag_validation: Skips internal validation of accessioning tags prior to sample accessioning y25_705_notify_on_internal_accessioning_validation_failures: Sends email to developers when internal validation fails during accessioning y25_705_notify_on_external_accessioning_validation_failures: Sends email to developers when external validation fails during accessioning -y25_105_validate_descriptor_required_field: Enables validation of batch workflow task descriptor required field From c12b75b326aaa2f04ca2ad82d5ed4469d0a310bd Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:00:27 +0000 Subject: [PATCH 106/125] Update vite-plugin-ruby to version 5.2.1 --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4b4d3ca672..735996b539 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2198,9 +2198,9 @@ util-deprecate@^1.0.2, util-deprecate@~1.0.1: integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== "vite-plugin-ruby@>=5.1.0 <5.1.2 || ^5.1.3 || ^5.2": - version "5.2.0" - resolved "https://registry.yarnpkg.com/vite-plugin-ruby/-/vite-plugin-ruby-5.2.0.tgz#a033be47e0531e842d962df9ef394c6e7f940a6a" - integrity sha512-FoCaok2pV7GrcAqdxniI1r5XWBlSg9HwEwaxdQdXUVFfYkyINVakPeyrSK4PqOVhonBCuoc633g6bDTEC7wkcA== + version "5.2.1" + resolved "https://registry.yarnpkg.com/vite-plugin-ruby/-/vite-plugin-ruby-5.2.1.tgz#8422e2a69cd923b5d63f510744f0503a622bf4c6" + integrity sha512-wI3F/Yr4e4mEwiMff/cvNwGu8nZok5wrwUjHxO8we+h3y9+qCluO3Y5dzvz6vHJDBya9fKXkltoMwoJhaB2SRg== dependencies: obug "^2.0" tinyglobby "^0.2.12" From e70da596a60799425e66e8182cacbbdb821fd437 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 15 Apr 2026 11:12:37 +0100 Subject: [PATCH 107/125] fix: use corrrect fields list for batch payloads --- config/accession/notification-template.mjml | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/config/accession/notification-template.mjml b/config/accession/notification-template.mjml index ec23193759..34d50bc2b5 100644 --- a/config/accession/notification-template.mjml +++ b/config/accession/notification-template.mjml @@ -59,13 +59,13 @@ A batch of - {{ fields.samples | length }} {{ fields | length }}  samples have failed to accession for study - {{ fields.study_name }} {{ fields[0]["study_name"] }}  and manifest - {{ fields.manifest_id }} {{ fields[0]["manifest_id"] }} .
Please review and address the warnings below. @@ -98,8 +98,8 @@ {% set ns = namespace(reasons=[]) %} - {% for sample in fields.samples %} - {% for reason in sample.failure_groups %} + {% for item in fields %} + {% for reason in item["failure_groups"] %} {% if reason not in ns.reasons %} {% set ns.reasons = ns.reasons + [reason] %} {% endif %} @@ -108,14 +108,14 @@ {% for reason in ns.reasons %} {% set ns2 = namespace(count=0) %} - {% for sample in fields.samples %} - {% if reason in sample.failure_groups %} + {% for item in fields %} + {% if reason in item["failure_groups"] %} {% set ns2.count = ns2.count + 1 %} {% endif %} {% endfor %}
  • {{ reason }}: {{ ns2.count }} samples
  • - {% endfor %} + {% endfor %}
    @@ -126,22 +126,22 @@ First  - {{ [fields.samples | length, 5] | min }} + {{ [fields | length, 5] | min }}  failures:
    - {% for sample in fields.samples[:5] %} + {% for item in fields[:5] %} - {{ sample.sample_name }} ({{ sample.supplier_sample_name }}) + {{ item["sample_name"] }} ({{ item["supplier_sample_name"] }}) - {{ sample.status_message }} + {{ item["accessioning_status_message"] }} From a1dbc63c9218e8759888496fb85e8b52521b2688 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 15 Apr 2026 11:57:50 +0100 Subject: [PATCH 108/125] fix: remove extraneous spacing --- config/accession/notification-template.mjml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/config/accession/notification-template.mjml b/config/accession/notification-template.mjml index 34d50bc2b5..ebdaf44644 100644 --- a/config/accession/notification-template.mjml +++ b/config/accession/notification-template.mjml @@ -59,13 +59,13 @@ A batch of - {{ fields | length }}  samples have failed to accession for study + {{ fields | length }} samples have failed to accession for study - {{ fields[0]["study_name"] }}  and manifest + {{ fields[0]["study_name"] }} and manifest - {{ fields[0]["manifest_id"] }} {{ fields[0]["manifest_id"] }}.
    Please review and address the warnings below. @@ -125,9 +125,9 @@ - First  + First {{ [fields | length, 5] | min }} -  failures: + failures: From 8d9f292fd4b8ba8e14ce32375314054369d0aaf4 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Wed, 15 Apr 2026 12:28:18 +0100 Subject: [PATCH 109/125] style: add enviroment dependent note --- config/accession/notification-template.mjml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/accession/notification-template.mjml b/config/accession/notification-template.mjml index ebdaf44644..35793042de 100644 --- a/config/accession/notification-template.mjml +++ b/config/accession/notification-template.mjml @@ -46,6 +46,7 @@ + From 9b21da7f34ab304300bc6867913fed03b5068b6e Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Mon, 13 Apr 2026 15:51:26 +0100 Subject: [PATCH 110/125] fix(labwhere-client): handle request timeouts --- lib/lab_where_client.rb | 6 +++--- spec/lib/lab_where_client_spec.rb | 28 ++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/lab_where_client.rb b/lib/lab_where_client.rb index 9638bcfa13..2e04600a4c 100644 --- a/lib/lab_where_client.rb +++ b/lib/lab_where_client.rb @@ -26,13 +26,13 @@ def parse_json(str) def get(instance, target) parse_json(RestClient.get(path_to(instance, target))) - rescue Errno::ECONNREFUSED, RestClient::NotFound => e + rescue Errno::ECONNREFUSED, RestClient::NotFound, RestClient::RequestTimeout => e raise LabwhereException.new(e), 'LabWhere service is down', e.backtrace end def post(instance, target, payload) parse_json(RestClient.post(path_to(instance, target), payload)) - rescue Errno::ECONNREFUSED, RestClient::NotFound => e + rescue Errno::ECONNREFUSED, RestClient::NotFound, RestClient::RequestTimeout => e raise LabwhereException.new(e), 'LabWhere service is down', e.backtrace rescue RestClient::UnprocessableEntity => e parse_json(e.response) @@ -40,7 +40,7 @@ def post(instance, target, payload) def put(instance, target, payload) parse_json(RestClient.put(path_to(instance, target), payload)) - rescue Errno::ECONNREFUSED, RestClient::NotFound => e + rescue Errno::ECONNREFUSED, RestClient::NotFound, RestClient::RequestTimeout => e raise LabwhereException.new(e), 'LabWhere service is down', e.backtrace end end diff --git a/spec/lib/lab_where_client_spec.rb b/spec/lib/lab_where_client_spec.rb index 4f4a40b680..436312d388 100644 --- a/spec/lib/lab_where_client_spec.rb +++ b/spec/lib/lab_where_client_spec.rb @@ -4,6 +4,10 @@ RSpec.describe LabWhereClient do describe LabWhereClient::Scan do + before { configatron.labwhere_api = 'https://labwhere.example.com/api' } + # Reset the configatron value after the test to avoid affecting other tests + after { configatron.labwhere_api = nil } + let(:scan_params) { { 'message' => 'Scan successful', 'errors' => nil } } let(:scan) { described_class.new(scan_params) } @@ -41,10 +45,26 @@ ) end - it 'raises an error when Labwhere is down' do - labwhere = instance_double(LabWhereClient::LabWhere) - allow(LabWhereClient::LabWhere).to receive(:new).and_return(labwhere) - allow(labwhere).to receive(:post).and_raise(LabWhereClient::LabwhereException.new, 'LabWhere service is down') + it 'propagates labwhere errors when receieving unprocessible entity errors' do + error_response = RestClient::UnprocessableEntity.new + allow(error_response).to receive(:response).and_return({ errors: 'Invalid data' }.to_json) + allow(RestClient).to receive(:post).and_raise(error_response) + + scan = described_class.create(location_barcode: '123', user_code: '456', labware_barcodes: ['789']) + expect(scan.valid?).to be false + expect(scan.errors).to eq('Invalid data') + end + + it 'raises an error when Labwhere is unreachable' do + allow(RestClient).to receive(:post).and_raise(Errno::ECONNREFUSED) + + expect do + described_class.create(location_barcode: '123', user_code: '456', labware_barcodes: ['789']) + end.to raise_error(LabWhereClient::LabwhereException, 'LabWhere service is down') + end + + it 'raises an error on other rest client error types' do + allow(RestClient).to receive(:post).and_raise(RestClient::Exceptions::OpenTimeout) expect do described_class.create(location_barcode: '123', user_code: '456', labware_barcodes: ['789']) From 38d32a546dd3dad3aa5518b855580466010bb727 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:15:20 +0000 Subject: [PATCH 111/125] Update test-prof to version 1.6.1 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9e483c2e0d..3709293e2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -603,7 +603,7 @@ GEM syslog (0.4.0) logger temple (0.10.4) - test-prof (1.6.0) + test-prof (1.6.1) thor (1.5.0) tilt (2.6.1) timecop (0.9.10) From 348161c051aea68d641034ce0be7c7b4307dcc4c Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Mon, 13 Apr 2026 11:17:08 +0100 Subject: [PATCH 112/125] feat(limber-bespoke-pcr): adds Custom 2-step amplicon PCR library type --- config/default_records/request_types/005_limber_bespoke.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/default_records/request_types/005_limber_bespoke.yml b/config/default_records/request_types/005_limber_bespoke.yml index 36969088f0..09ad8b31cc 100644 --- a/config/default_records/request_types/005_limber_bespoke.yml +++ b/config/default_records/request_types/005_limber_bespoke.yml @@ -45,6 +45,7 @@ limber_pcr_bespoke: - TraDIS - TruSeq mRNA (RNA Seq) - SGE Library v0.2 + - Custom 2-step amplicon PCR limber_chromium_bespoke: <<: *limber_bespoke_library name: Limber Chromium Bespoke From 987b357aafeed3f4b351227ef309fc4864dfb586 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 16 Apr 2026 10:09:56 +0100 Subject: [PATCH 113/125] build: upgrade vitejs/plugin-legacy --- package.json | 2 +- yarn.lock | 1071 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 1037 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 50cd959a87..25dac05270 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "terser": "^5.46.1" }, "devDependencies": { - "@vitejs/plugin-legacy": "^1.8", + "@vitejs/plugin-legacy": "^5.0", "eslint": "^9.19", "eslint-config-prettier": "^8.10", "globals": "^15.14", diff --git a/yarn.lock b/yarn.lock index 4b4d3ca672..70d10f82fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13,10 +13,787 @@ "@csstools/css-tokenizer" "^3.0.3" lru-cache "^10.4.3" -"@babel/standalone@^7.17.11": - version "7.28.3" - resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.28.3.tgz#0e126deef7b88b08481c233c7ac0e94c9629d7cd" - integrity sha512-VHmaaU23OkxShTtkwXlte7/uHDK8v55J9YLMqlucjnYujeB9YgrYCHU6LREqUegTVq+/KlLgjoUu8lbeI3XQPA== +"@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.28.6", "@babel/compat-data@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== + +"@babel/core@^7.25.8": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.29.0": + version "7.29.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" + integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== + dependencies: + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== + dependencies: + "@babel/types" "^7.27.3" + +"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz#611ff5482da9ef0db6291bcd24303400bca170fb" + integrity sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.28.6" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1", "@babel/helper-create-regexp-features-plugin@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz#7c1ddd64b2065c7f78034b25b43346a7e19ed997" + integrity sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + regexpu-core "^6.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.8": + version "0.6.8" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz#cf1e4462b613f2b54c41e6ff758d5dfcaa2c85d1" + integrity sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA== + dependencies: + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + debug "^4.4.3" + lodash.debounce "^4.0.8" + resolve "^1.22.11" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-member-expression-to-functions@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" + integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== + dependencies: + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + +"@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== + dependencies: + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" + +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" + integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== + +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-replace-supers@^7.27.1", "@babel/helper-replace-supers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz#94aa9a1d7423a00aead3f204f78834ce7d53fe44" + integrity sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.28.6" + +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helper-wrap-function@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz#4e349ff9222dab69a93a019cc296cdd8442e279a" + integrity sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ== + dependencies: + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helpers@^7.28.6": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.2.tgz#9cfbccb02b8e229892c0b07038052cc1a8709c49" + integrity sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw== + dependencies: + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + +"@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.2.tgz#58bd50b9a7951d134988a1ae177a35ef9a703ba1" + integrity sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA== + dependencies: + "@babel/types" "^7.29.0" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz#fbde57974707bbfa0376d34d425ff4fa6c732421" + integrity sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.5" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz#43f70a6d7efd52370eefbdf55ae03d91b293856d" + integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz#beb623bd573b8b6f3047bd04c32506adc3e58a72" + integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz#e134a5479eb2ba9c02714e8c1ebf1ec9076124fd" + integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz#0e8289cec28baaf05d54fd08d81ae3676065f69f" + integrity sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/traverse" "^7.28.6" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-import-assertions@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz#ae9bc1923a6ba527b70104dd2191b0cd872c8507" + integrity sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-import-attributes@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz#b71d5914665f60124e133696f17cd7669062c503" + integrity sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-async-generator-functions@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz#63ed829820298f0bf143d5a4a68fb8c06ffd742f" + integrity sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.29.0" + +"@babel/plugin-transform-async-to-generator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz#bd97b42237b2d1bc90d74bcb486c39be5b4d7e77" + integrity sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-remap-async-to-generator" "^7.27.1" + +"@babel/plugin-transform-block-scoped-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz#558a9d6e24cf72802dd3b62a4b51e0d62c0f57f9" + integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-block-scoping@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz#e1ef5633448c24e76346125c2534eeb359699a99" + integrity sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-class-properties@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz#d274a4478b6e782d9ea987fda09bdb6d28d66b72" + integrity sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-class-static-block@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz#1257491e8259c6d125ac4d9a6f39f9d2bf3dba70" + integrity sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-classes@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz#8f6fb79ba3703978e701ce2a97e373aae7dda4b7" + integrity sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-globals" "^7.28.0" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-replace-supers" "^7.28.6" + "@babel/traverse" "^7.28.6" + +"@babel/plugin-transform-computed-properties@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz#936824fc71c26cb5c433485776d79c8e7b0202d2" + integrity sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/template" "^7.28.6" + +"@babel/plugin-transform-destructuring@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz#b8402764df96179a2070bb7b501a1586cf8ad7a7" + integrity sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.5" + +"@babel/plugin-transform-dotall-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz#def31ed84e0fb6e25c71e53c124e7b76a4ab8e61" + integrity sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-duplicate-keys@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz#f1fbf628ece18e12e7b32b175940e68358f546d1" + integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz#8014b8a6cfd0e7b92762724443bf0d2400f26df1" + integrity sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-dynamic-import@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz#4c78f35552ac0e06aa1f6e3c573d67695e8af5a4" + integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-explicit-resource-management@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz#dd6788f982c8b77e86779d1d029591e39d9d8be7" + integrity sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" + +"@babel/plugin-transform-exponentiation-operator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz#5e477eb7eafaf2ab5537a04aaafcf37e2d7f1091" + integrity sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-export-namespace-from@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-for-of@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-function-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== + dependencies: + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-transform-json-strings@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz#4c8c15b2dc49e285d110a4cf3dac52fd2dfc3038" + integrity sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-logical-assignment-operators@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz#53028a3d77e33c50ef30a8fce5ca17065936e605" + integrity sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-member-expression-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz#37b88ba594d852418e99536f5612f795f23aeaf9" + integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-modules-amd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz#a4145f9d87c2291fe2d05f994b65dba4e3e7196f" + integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-modules-commonjs@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz#c0232e0dfe66a734cc4ad0d5e75fc3321b6fdef1" + integrity sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA== + dependencies: + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-modules-systemjs@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz#e458a95a17807c415924106a3ff188a3b8dee964" + integrity sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ== + dependencies: + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.29.0" + +"@babel/plugin-transform-modules-umd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz#63f2cf4f6dc15debc12f694e44714863d34cd334" + integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz#a26cd51e09c4718588fc4cce1c5d1c0152102d6a" + integrity sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-new-target@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz#259c43939728cad1706ac17351b7e6a7bea1abeb" + integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz#9bc62096e90ab7a887f3ca9c469f6adec5679757" + integrity sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-numeric-separator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz#1310b0292762e7a4a335df5f580c3320ee7d9e9f" + integrity sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-object-rest-spread@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz#fdd4bc2d72480db6ca42aed5c051f148d7b067f7" + integrity sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA== + dependencies: + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/traverse" "^7.28.6" + +"@babel/plugin-transform-object-super@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz#1c932cd27bf3874c43a5cac4f43ebf970c9871b5" + integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + +"@babel/plugin-transform-optional-catch-binding@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz#75107be14c78385978201a49c86414a150a20b4c" + integrity sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-optional-chaining@^7.27.1", "@babel/plugin-transform-optional-chaining@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz#926cf150bd421fc8362753e911b4a1b1ce4356cd" + integrity sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-parameters@^7.27.7": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz#1fd2febb7c74e7d21cf3b05f7aebc907940af53a" + integrity sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-private-methods@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz#c76fbfef3b86c775db7f7c106fff544610bdb411" + integrity sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-private-property-in-object@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz#4fafef1e13129d79f1d75ac180c52aafefdb2811" + integrity sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-property-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz#07eafd618800591e88073a0af1b940d9a42c6424" + integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-regenerator@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz#dec237cec1b93330876d6da9992c4abd42c9d18b" + integrity sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-regexp-modifiers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz#7ef0163bd8b4a610481b2509c58cf217f065290b" + integrity sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-reserved-words@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz#40fba4878ccbd1c56605a4479a3a891ac0274bb4" + integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-shorthand-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-spread@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz#40a2b423f6db7b70f043ad027a58bcb44a9757b6" + integrity sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-sticky-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-template-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-typeof-symbol@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz#70e966bb492e03509cf37eafa6dcc3051f844369" + integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-escapes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" + integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-property-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz#63a7a6c21a0e75dae9b1861454111ea5caa22821" + integrity sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-unicode-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-sets-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz#924912914e5df9fe615ec472f88ff4788ce04d4e" + integrity sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/preset-env@^7.25.8": + version "7.29.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.2.tgz#5a173f22c7d8df362af1c9fe31facd320de4a86c" + integrity sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw== + dependencies: + "@babel/compat-data" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.28.5" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.28.6" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.28.6" + "@babel/plugin-syntax-import-attributes" "^7.28.6" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.27.1" + "@babel/plugin-transform-async-generator-functions" "^7.29.0" + "@babel/plugin-transform-async-to-generator" "^7.28.6" + "@babel/plugin-transform-block-scoped-functions" "^7.27.1" + "@babel/plugin-transform-block-scoping" "^7.28.6" + "@babel/plugin-transform-class-properties" "^7.28.6" + "@babel/plugin-transform-class-static-block" "^7.28.6" + "@babel/plugin-transform-classes" "^7.28.6" + "@babel/plugin-transform-computed-properties" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" + "@babel/plugin-transform-dotall-regex" "^7.28.6" + "@babel/plugin-transform-duplicate-keys" "^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.29.0" + "@babel/plugin-transform-dynamic-import" "^7.27.1" + "@babel/plugin-transform-explicit-resource-management" "^7.28.6" + "@babel/plugin-transform-exponentiation-operator" "^7.28.6" + "@babel/plugin-transform-export-namespace-from" "^7.27.1" + "@babel/plugin-transform-for-of" "^7.27.1" + "@babel/plugin-transform-function-name" "^7.27.1" + "@babel/plugin-transform-json-strings" "^7.28.6" + "@babel/plugin-transform-literals" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.28.6" + "@babel/plugin-transform-member-expression-literals" "^7.27.1" + "@babel/plugin-transform-modules-amd" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.28.6" + "@babel/plugin-transform-modules-systemjs" "^7.29.0" + "@babel/plugin-transform-modules-umd" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.29.0" + "@babel/plugin-transform-new-target" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.28.6" + "@babel/plugin-transform-numeric-separator" "^7.28.6" + "@babel/plugin-transform-object-rest-spread" "^7.28.6" + "@babel/plugin-transform-object-super" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding" "^7.28.6" + "@babel/plugin-transform-optional-chaining" "^7.28.6" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/plugin-transform-private-methods" "^7.28.6" + "@babel/plugin-transform-private-property-in-object" "^7.28.6" + "@babel/plugin-transform-property-literals" "^7.27.1" + "@babel/plugin-transform-regenerator" "^7.29.0" + "@babel/plugin-transform-regexp-modifiers" "^7.28.6" + "@babel/plugin-transform-reserved-words" "^7.27.1" + "@babel/plugin-transform-shorthand-properties" "^7.27.1" + "@babel/plugin-transform-spread" "^7.28.6" + "@babel/plugin-transform-sticky-regex" "^7.27.1" + "@babel/plugin-transform-template-literals" "^7.27.1" + "@babel/plugin-transform-typeof-symbol" "^7.27.1" + "@babel/plugin-transform-unicode-escapes" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex" "^7.28.6" + "@babel/plugin-transform-unicode-regex" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.28.6" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.15" + babel-plugin-polyfill-corejs3 "^0.14.0" + babel-plugin-polyfill-regenerator "^0.6.6" + core-js-compat "^3.48.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/template@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + debug "^4.3.1" + +"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.29.0", "@babel/types@^7.4.4": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" "@csstools/color-helpers@^5.0.2": version "5.0.2" @@ -368,7 +1145,7 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== -"@jridgewell/gen-mapping@^0.3.5": +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== @@ -376,6 +1153,14 @@ "@jridgewell/sourcemap-codec" "^1.5.0" "@jridgewell/trace-mapping" "^0.3.24" +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" @@ -389,7 +1174,7 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" -"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": version "1.5.5" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== @@ -402,6 +1187,14 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@parcel/watcher-android-arm64@2.5.1": version "2.5.1" resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" @@ -611,16 +1404,19 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@vitejs/plugin-legacy@^1.8": - version "1.8.2" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-legacy/-/plugin-legacy-1.8.2.tgz#2f315bcb6685b12719813fb9412851445cca636f" - integrity sha512-NCOKU+pU+cxLMR9P9RTolEuOK+h+zYBXlknj+zGcKSj/NXBZYgA1GAH1FnO4zijoWRiTaiOm2ha9LQrELE7XHg== +"@vitejs/plugin-legacy@^5.0": + version "5.4.3" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-legacy/-/plugin-legacy-5.4.3.tgz#9ba634d25fb56e350cb0f576ce88099218b3b9b9" + integrity sha512-wsyXK9mascyplcqvww1gA1xYiy29iRHfyciw+a0t7qRNdzX6PdfSWmOoCi74epr87DujM+5J+rnnSv+4PazqVg== dependencies: - "@babel/standalone" "^7.17.11" - core-js "^3.22.3" - magic-string "^0.26.1" - regenerator-runtime "^0.13.9" - systemjs "^6.12.1" + "@babel/core" "^7.25.8" + "@babel/preset-env" "^7.25.8" + browserslist "^4.24.0" + browserslist-to-esbuild "^2.1.1" + core-js "^3.38.1" + magic-string "^0.30.12" + regenerator-runtime "^0.14.1" + systemjs "^6.15.1" acorn-jsx@^5.3.2: version "5.3.2" @@ -671,11 +1467,40 @@ autoprefixer@^10.4.13: picocolors "^1.1.1" postcss-value-parser "^4.2.0" +babel-plugin-polyfill-corejs2@^0.4.15: + version "0.4.17" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz#198f970f1c99a856b466d1187e88ce30bd199d91" + integrity sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-define-polyfill-provider" "^0.6.8" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.14.0: + version "0.14.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz#6ac08d2f312affb70c4c69c0fbba4cb417ee5587" + integrity sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.8" + core-js-compat "^3.48.0" + +babel-plugin-polyfill-regenerator@^0.6.6: + version "0.6.8" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz#8a6bfd5dd54239362b3d06ce47ac52b2d95d7721" + integrity sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.8" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +baseline-browser-mapping@^2.10.12: + version "2.10.19" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz#7697721c22f94f66195d0c34299b1a91e3299493" + integrity sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g== + bootstrap@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.2.tgz#8e0cd61611728a5bf65a3a2b8d6ff6c77d5d7479" @@ -696,6 +1521,13 @@ braces@^3.0.3: dependencies: fill-range "^7.1.1" +browserslist-to-esbuild@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/browserslist-to-esbuild/-/browserslist-to-esbuild-2.1.1.tgz#50dc4c55a6889ba22c7b1bd820032f81b822faf0" + integrity sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw== + dependencies: + meow "^13.0.0" + browserslist@^4.21.4, browserslist@^4.24.4: version "4.25.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.3.tgz#9167c9cbb40473f15f75f85189290678b99b16c5" @@ -706,6 +1538,17 @@ browserslist@^4.21.4, browserslist@^4.24.4: node-releases "^2.0.19" update-browserslist-db "^1.1.3" +browserslist@^4.24.0, browserslist@^4.28.1: + version "4.28.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.2.tgz#f50b65362ef48974ca9f50b3680566d786b811d2" + integrity sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg== + dependencies: + baseline-browser-mapping "^2.10.12" + caniuse-lite "^1.0.30001782" + electron-to-chromium "^1.5.328" + node-releases "^2.0.36" + update-browserslist-db "^1.2.3" + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -721,6 +1564,11 @@ caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001735: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001736.tgz#3710a99cf154b653590fb6a57f81ee34173c3b47" integrity sha512-ImpN5gLEY8gWeqfLUyEF4b7mYWcYoR2Si1VhnrbM4JizRFmfGaAQ12PhNykq6nvI4XvKLrsp8Xde74D5phJOSw== +caniuse-lite@^1.0.30001782: + version "1.0.30001788" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz#31e97d1bfec332b3f2d7eea7781460c97629b3bf" + integrity sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ== + chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -763,10 +1611,22 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -core-js@^3.22.3: - version "3.45.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.45.1.tgz#5810e04a1b4e9bc5ddaa4dd12e702ff67300634d" - integrity sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg== +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +core-js-compat@^3.48.0: + version "3.49.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.49.0.tgz#06145447d92f4aaf258a0c44f24b47afaeaffef6" + integrity sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA== + dependencies: + browserslist "^4.28.1" + +core-js@^3.38.1: + version "3.49.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.49.0.tgz#8b4d520ac034311fa21aa616f017ada0e0dbbddd" + integrity sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg== core-util-is@~1.0.0: version "1.0.3" @@ -965,6 +1825,13 @@ debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: dependencies: ms "^2.1.3" +debug@^4.1.0, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + decimal.js@^10.5.0: version "10.6.0" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" @@ -985,11 +1852,21 @@ electron-to-chromium@^1.5.204: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.207.tgz#0fedde3eec615065ee95531c09a10578644c5552" integrity sha512-mryFrrL/GXDTmAtIVMVf+eIXM09BBPlO5IQ7lUyKmK8d+A4VpRGG+M3ofoVef6qyF8s60rJei8ymlJxjUA8Faw== +electron-to-chromium@^1.5.328: + version "1.5.339" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.339.tgz#d797bf5f222a7f6241a42b43a97bf52ff43947f1" + integrity sha512-Is+0BBHJ4NrdpAYiperrmp53pLywG/yV/6lIMTAnhxvzj/Cmn5Q/ogSHC6AKe7X+8kPLxxFk0cs5oc/3j/fxIg== + entities@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + esbuild@^0.21.3: version "0.21.5" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" @@ -1195,6 +2072,11 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" @@ -1292,7 +2174,7 @@ inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -is-core-module@^2.16.0: +is-core-module@^2.16.0, is-core-module@^2.16.1: version "2.16.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== @@ -1336,6 +2218,11 @@ jquery@>=1.7, jquery@^3.7.1: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de" integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -1374,6 +2261,11 @@ jsdom@^26.0: ws "^8.18.0" xml-name-validator "^5.0.0" +jsesc@^3.0.2, jsesc@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -1389,6 +2281,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + jszip@^3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" @@ -1428,6 +2325,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -1438,12 +2340,24 @@ lru-cache@^10.4.3: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== -magic-string@^0.26.1: - version "0.26.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.7.tgz#caf7daf61b34e9982f8228c4527474dac8981d6f" - integrity sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow== +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +magic-string@^0.30.12: + version "0.30.21" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" + integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== dependencies: - sourcemap-codec "^1.4.8" + "@jridgewell/sourcemap-codec" "^1.5.5" + +meow@^13.0.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" + integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA== micromatch@^4.0.5: version "4.0.8" @@ -1485,6 +2399,11 @@ node-releases@^2.0.19: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== +node-releases@^2.0.36: + version "2.0.37" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.37.tgz#9bd4f10b77ba39c2b9402d4e8399c482a797f671" + integrity sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg== + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" @@ -1949,10 +2868,46 @@ readdirp@^4.0.1: resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== -regenerator-runtime@^0.13.9: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerate-unicode-properties@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz#aa113812ba899b630658c7623466be71e1f86f66" + integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.14.1: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regexpu-core@^6.3.1: + version "6.4.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.4.0.tgz#3580ce0c4faedef599eccb146612436b62a176e5" + integrity sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.2" + regjsgen "^0.8.0" + regjsparser "^0.13.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.2.1" + +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.13.0: + version "0.13.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.13.1.tgz#0593cbacb27527927692030928ae4d3b878d6f8d" + integrity sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw== + dependencies: + jsesc "~3.1.0" resolve-from@^4.0.0: version "4.0.0" @@ -1968,6 +2923,16 @@ resolve@^1.1.7: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.11: + version "1.22.12" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + rollup@^4.20.0: version "4.47.1" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.47.1.tgz#c40bce25b7140265dbe5467cd32871f71e9f9f0b" @@ -2035,6 +3000,11 @@ select2@^4.1.0-rc.0: resolved "https://registry.yarnpkg.com/select2/-/select2-4.1.0-rc.0.tgz#ba3cd3901dda0155e1c0219ab41b74ba51ea22d8" integrity sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^7.5.4: version "7.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" @@ -2080,11 +3050,6 @@ source-map@^0.6.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -sourcemap-codec@^1.4.8: - version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -2114,7 +3079,7 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -systemjs@^6.12.1: +systemjs@^6.15.1: version "6.15.1" resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.15.1.tgz#74175b6810e27a79e1177d21db5f0e3057118cea" integrity sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA== @@ -2177,6 +3142,29 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz#65a7adfad8574c219890e219285ce4c64ed67eaa" + integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz#301d4f8a43d2b75c97adfad87c9dd5350c9475d1" + integrity sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ== + update-browserslist-db@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" @@ -2185,6 +3173,14 @@ update-browserslist-db@^1.1.3: escalade "^3.2.0" picocolors "^1.1.1" +update-browserslist-db@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -2275,6 +3271,11 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 5408d5b1d29f29e8bf3006deb664b4c45a6ac6c2 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Thu, 16 Apr 2026 10:10:25 +0100 Subject: [PATCH 114/125] build: upgrade eslint-config-prettier --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 25dac05270..cd6430722a 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "devDependencies": { "@vitejs/plugin-legacy": "^5.0", "eslint": "^9.19", - "eslint-config-prettier": "^8.10", + "eslint-config-prettier": "^9.0", "globals": "^15.14", "jsdom": "^26.0", "prettier": "^3.3", diff --git a/yarn.lock b/yarn.lock index 70d10f82fe..121ad2479a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1906,10 +1906,10 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-prettier@^8.10: - version "8.10.2" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz#0642e53625ebc62c31c24726b0f050df6bd97a2e" - integrity sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A== +eslint-config-prettier@^9.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz#90deb4fa0259592df774b600dbd1d2249a78ce91" + integrity sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ== eslint-scope@^8.4.0: version "8.4.0" From d0ea465c3685377c0ecc0e030790963638584b76 Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Thu, 16 Apr 2026 10:53:34 +0100 Subject: [PATCH 115/125] ci: only run test ci on push on develop/master --- .github/workflows/cucumber_tests.yml | 3 +++ .github/workflows/lint.yml | 3 +++ .github/workflows/rake_tests.yml | 3 +++ .github/workflows/rspec_feature_tests.yml | 3 +++ .github/workflows/rspec_unit_tests.yml | 3 +++ 5 files changed, 15 insertions(+) diff --git a/.github/workflows/cucumber_tests.yml b/.github/workflows/cucumber_tests.yml index b5d080d043..32c34d65d7 100644 --- a/.github/workflows/cucumber_tests.yml +++ b/.github/workflows/cucumber_tests.yml @@ -17,6 +17,9 @@ env: on: push: + branches: + - develop + - master pull_request: types: # defaults diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3c07524d38..e3bec5df12 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,6 +2,9 @@ name: Lint on: push: + branches: + - develop + - master pull_request: types: # defaults diff --git a/.github/workflows/rake_tests.yml b/.github/workflows/rake_tests.yml index 8858329514..9dabef0b20 100644 --- a/.github/workflows/rake_tests.yml +++ b/.github/workflows/rake_tests.yml @@ -14,6 +14,9 @@ env: on: push: + branches: + - develop + - master pull_request: types: # defaults diff --git a/.github/workflows/rspec_feature_tests.yml b/.github/workflows/rspec_feature_tests.yml index 148010ef76..184fd8563d 100644 --- a/.github/workflows/rspec_feature_tests.yml +++ b/.github/workflows/rspec_feature_tests.yml @@ -17,6 +17,9 @@ env: on: push: + branches: + - develop + - master pull_request: types: # defaults diff --git a/.github/workflows/rspec_unit_tests.yml b/.github/workflows/rspec_unit_tests.yml index fa8be7b232..58de58a87a 100644 --- a/.github/workflows/rspec_unit_tests.yml +++ b/.github/workflows/rspec_unit_tests.yml @@ -17,6 +17,9 @@ env: on: push: + branches: + - develop + - master pull_request: types: # defaults From 4f1209f3e361a4f5583567a1d2d746a7238c2aba Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Thu, 16 Apr 2026 11:26:56 +0100 Subject: [PATCH 116/125] ci: update code cov build number check --- codecov.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/codecov.yml b/codecov.yml index f0cf57aeb3..d2662e06e2 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,12 +1,8 @@ codecov: notify: # do not notify until at least n builds have been uploaded from the CI pipeline - # rake_tests (push) # rake_tests (pull_request) - # rspec_unit_tests x4 (push) # rspec_unit_tests x4 (pull_request) - # rspec_feature_tests x2 (push) # rspec_feature_tests x2 (pull_request) - # cucumber_tests x2 (push) # cucumber_tests x2 (pull_request) - after_n_builds: 18 + after_n_builds: 9 From f4ce90bfed708ecdbb65d0215142c677a49e050c Mon Sep 17 00:00:00 2001 From: Ben Topping Date: Thu, 16 Apr 2026 13:34:30 +0100 Subject: [PATCH 117/125] fix: adds automated true to ug200 sequencing automated template --- .../021_ultima_ug200_submission_templates.wip.yml | 1 + 1 file changed, 1 insertion(+) 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 5c534184d0..c1758e5c98 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 @@ -11,6 +11,7 @@ Limber-Htp - Ultima UG200 PCR Free - Ultima UG200 sequencing: product_catalogue_name: GenericNoPCR Limber-Htp - Ultima UG200 PCR Free - Ultima UG200 sequencing Automated: submission_class_name: "LinearSubmission" + automated: true related_records: request_type_keys: ["ultima_ug200_sequencing"] order_role: PCR Free From 821272b0b41933398501569800eeb5ff8270f9bb Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:00:20 +0000 Subject: [PATCH 118/125] Update selenium-webdriver to version 4.43.0 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9e483c2e0d..ee9aa1192b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -560,7 +560,7 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.16.8) securerandom (0.4.1) - selenium-webdriver (4.41.0) + selenium-webdriver (4.43.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) From a1f590f96b9fba2d495d16ab3dc17a20a37e444e Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:50:13 +0000 Subject: [PATCH 119/125] Update prettier to version 3.8.3 --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4b4d3ca672..5ee9fb1f98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1910,9 +1910,9 @@ prelude-ls@^1.2.1: integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== prettier@^3.3: - version "3.8.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" - integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== + version "3.8.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.3.tgz#560f2de55bf01b4c0503bc629d5df99b9a1d09b0" + integrity sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw== process-nextick-args@~2.0.0: version "2.0.1" From e8a60cb63920a66011f2766c0a7e02e089d677de Mon Sep 17 00:00:00 2001 From: Wendy Yang Date: Fri, 17 Apr 2026 14:04:27 +0100 Subject: [PATCH 120/125] remove the wip of lcm_triomis_new_added_submission_templates.yml --- ...ip.yml => 020_lcm_triomics_new_added_submission_templates.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config/default_records/submission_templates/{020_lcm_triomics_new_added_submission_templates.wip.yml => 020_lcm_triomics_new_added_submission_templates.yml} (100%) diff --git a/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml b/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.yml similarity index 100% rename from config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml rename to config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.yml From e7519ceb6beb002f665b6ae4ce933bfff8f1fa43 Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 00:05:57 +0000 Subject: [PATCH 121/125] Update yard to version 0.9.42 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3a07bfd3ad..16638f4b9b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -646,7 +646,7 @@ GEM will_paginate (>= 3.0.3) xpath (3.2.0) nokogiri (~> 1.8) - yard (0.9.38) + yard (0.9.42) yard-activerecord (0.0.17) activesupport yard (>= 0.8.3) From 0daba54beced8befe61c0ebf9fbe3a2aca83433c Mon Sep 17 00:00:00 2001 From: "depfu[bot]" <23717796+depfu[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:00:20 +0000 Subject: [PATCH 122/125] Update timecop to version 0.9.11 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3a07bfd3ad..6d9a9c8d27 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -606,7 +606,7 @@ GEM test-prof (1.6.1) thor (1.5.0) tilt (2.6.1) - timecop (0.9.10) + timecop (0.9.11) timeout (0.6.1) traceroute (0.8.1) rails (>= 3.0.0) From 58993a9fb992c312a19a8914c960f74ac47055b7 Mon Sep 17 00:00:00 2001 From: Stephen Hulme Date: Fri, 17 Apr 2026 15:39:00 +0100 Subject: [PATCH 123/125] style: update rubocop todo --- .rubocop_todo.yml | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index ea0487373f..e715df25f4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --no-exclude-limit` -# on 2026-04-08 13:41:45 UTC using RuboCop version 1.85.1. +# on 2026-04-20 08:03:49 UTC using RuboCop version 1.86.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -133,7 +133,7 @@ Lint/DuplicateMethods: - 'app/models/stock_stamper.rb' - 'lib/accession/tag.rb' -# Offense count: 62 +# Offense count: 61 # Configuration parameters: AllowComments, AllowEmptyLambdas. Lint/EmptyBlock: Exclude: @@ -186,7 +186,6 @@ Lint/EmptyBlock: - 'test/factories/location_report_factories.rb' - 'test/unit/data_release_test.rb' - 'test/unit/fragment_test.rb' - - 'test/unit/tasks/plate_transfer_task_test.rb' # Offense count: 1 # Configuration parameters: AllowComments. @@ -499,7 +498,7 @@ Naming/PredicatePrefix: - 'lib/has_behaviour.rb' - 'lib/manifest_util.rb' -# Offense count: 246 +# Offense count: 245 # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, normalcase, non_integer # AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 @@ -507,7 +506,6 @@ Naming/VariableNumber: Exclude: - 'app/controllers/quad_stamp_controller.rb' - 'app/models/transfer_request.rb' - - 'config/application.rb' - 'db/seeds/0015_transfer_templates.rb' - 'spec/api/asset_audit_spec.rb' - 'spec/controllers/api/v2/heron/tube_racks_controller_spec.rb' @@ -972,7 +970,7 @@ RSpec/MultipleDescribes: - 'spec/lib/label_printer/asset_labels_spec.rb' - 'spec/models/qc_result/qc_result_spec.rb' -# Offense count: 925 +# Offense count: 926 # Configuration parameters: Max. RSpec/MultipleExpectations: Exclude: @@ -1563,14 +1561,6 @@ Rails/InverseOf: - 'app/models/well.rb' - 'app/models/work_order.rb' -# Offense count: 4 -Rails/LexicallyScopedActionFilter: - Exclude: - - 'app/controllers/admin/programs_controller.rb' - - 'app/controllers/pipelines_controller.rb' - - 'app/controllers/projects_controller.rb' - - 'app/controllers/sdb/sample_manifests_controller.rb' - # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). Rails/NegateInclude: @@ -2049,7 +2039,7 @@ Style/NumericPredicate: - 'lib/deployed.rb' - 'lib/submission_serializer.rb' -# Offense count: 32 +# Offense count: 20 # Configuration parameters: AllowedClasses. Style/OneClassPerFile: Exclude: @@ -2062,14 +2052,6 @@ Style/OneClassPerFile: - 'lib/api_tools.rb' - 'lib/authenticated_test_helper.rb' - 'lib/informatics/test/helpers/authentication_helper.rb' - - 'spec/models/broadcast_event/broadcast_event_spec.rb' - - 'spec/support/api_v2_resource_matchers.rb' - - 'spec/uat_actions/uat_actions_spec.rb' - - 'test/controllers/authentication_controller_test.rb' - - 'test/lib/label_printer/labels_multiplication_test.rb' - - 'test/test_helper.rb' - - 'test/unit/eventful_entry_test.rb' - - 'test/unit/tasks/plate_transfer_task_test.rb' # Offense count: 18 # Configuration parameters: AllowedMethods. From 2c7f2bea18c48907ad15aef876ccdcdb632cc023 Mon Sep 17 00:00:00 2001 From: wendyyang Date: Mon, 20 Apr 2026 12:27:25 +0100 Subject: [PATCH 124/125] Update .release-version --- .release-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.release-version b/.release-version index 11566873c1..a081b7573c 100644 --- a/.release-version +++ b/.release-version @@ -1 +1 @@ -14.90.0 +14.90.1 From 44eb2aa5f9790b9e51ba2a17162c4cfc2073cc03 Mon Sep 17 00:00:00 2001 From: Wendy Yang Date: Tue, 21 Apr 2026 09:23:35 +0100 Subject: [PATCH 125/125] unwip rna request type config --- ...ip.yml => 025_limber_lcm_triomics_new_added_request_types.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename config/default_records/request_types/{025_limber_lcm_triomics_new_added_request_types.wip.yml => 025_limber_lcm_triomics_new_added_request_types.yml} (100%) diff --git a/config/default_records/request_types/025_limber_lcm_triomics_new_added_request_types.wip.yml b/config/default_records/request_types/025_limber_lcm_triomics_new_added_request_types.yml similarity index 100% rename from config/default_records/request_types/025_limber_lcm_triomics_new_added_request_types.wip.yml rename to config/default_records/request_types/025_limber_lcm_triomics_new_added_request_types.yml