diff --git a/.rubocop.yml b/.rubocop.yml index 5709cbcfd1..8bf76fb35a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -284,6 +284,10 @@ Style/QuotedSymbols: Exclude: - bin/* +# Keep consistency across attribute writers and readers +Style/RedundantSelf: + Enabled: false + # Files generated by Rails Style/StringLiterals: Exclude: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c018867fa9..9947efd7c6 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-22 15:46:09 UTC using RuboCop version 1.86.1. +# on 2026-05-05 14:43:06 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 @@ -301,12 +301,11 @@ Lint/UselessOr: - 'app/models/qcable/statemachine.rb' - 'app/models/ui_helper/summary.rb' -# Offense count: 11 +# Offense count: 7 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max. Metrics/AbcSize: Exclude: - 'app/controllers/api/v2/transfers_controller.rb' - - 'app/controllers/studies/information_controller.rb' - 'app/jobs/export_pool_xp_to_traction_job.rb' - 'app/models/accession_service/base_service.rb' - 'app/sample_manifest_excel/sample_manifest_excel/manifest_type_list.rb' @@ -334,7 +333,7 @@ Metrics/CyclomaticComplexity: - 'app/models/accession_service/base_service.rb' - 'lib/limber/helper.rb' -# Offense count: 19 +# Offense count: 18 # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Exclude: @@ -617,7 +616,7 @@ RSpec/BeforeAfterAll: - 'spec/sample_manifest_excel/worksheet_spec.rb' - 'spec/sequencescape_excel/worksheet_spec.rb' -# Offense count: 342 +# Offense count: 330 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: @@ -649,7 +648,6 @@ RSpec/ContextWording: - 'spec/models/barcode_spec.rb' - 'spec/models/broadcast_event/lab_event_spec.rb' - 'spec/models/broadcast_event/qc_assay_spec.rb' - - 'spec/models/bulk_submission_spec.rb' - 'spec/models/comment_spec.rb' - 'spec/models/illumina_htp/initial_stock_tube_purpose_spec.rb' - 'spec/models/location_report_form_spec.rb' @@ -840,7 +838,7 @@ RSpec/ExampleWording: - 'spec/sequencescape_excel/validation_spec.rb' - 'spec/sequencescape_excel/worksheet_spec.rb' -# Offense count: 27 +# Offense count: 26 RSpec/ExpectInHook: Exclude: - 'spec/controllers/labwhere_receptions_controller_spec.rb' @@ -1197,7 +1195,7 @@ RSpec/MultipleExpectations: - 'spec/views/labware/show_chromium_chip_spec.rb' - 'spec/views/samples/index_html_erb_spec.rb' -# Offense count: 243 +# Offense count: 249 # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. # SupportedStyles: always, named_only RSpec/NamedSubject: diff --git a/Gemfile b/Gemfile index 2c7017c5a1..bc295f5e31 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' group :default do gem 'bootsnap' - gem 'concurrent-ruby', '1.3.5' + gem 'concurrent-ruby' gem 'configatron' gem 'formtastic' gem 'rails', '~> 8.0.0' @@ -21,10 +21,6 @@ group :default do gem 'faraday-multipart' gem 'rest-client' # Deprecated, but still used in some places, replace with Faraday where possible - # Fix incompatibility with between Ruby 3.1 and Psych 4 (used for yaml) - # see https://stackoverflow.com/a/71192990 - gem 'psych', '< 4' - # State machine gem 'aasm' gem 'after_commit_everywhere', '~> 1.0' # Required by AASM @@ -34,7 +30,7 @@ group :default do # Provides bulk insert capabilities gem 'activerecord-import' - gem 'record_loader', git: 'https://github.com/sanger/record_loader', tag: 'v0.3.0' + gem 'record_loader', git: 'https://github.com/sanger/record_loader', tag: 'v1.1.0' gem 'mysql2', platforms: :mri gem 'will_paginate' diff --git a/Gemfile.lock b/Gemfile.lock index 6a055aa724..93b9cd7fa2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,10 +16,11 @@ GIT GIT remote: https://github.com/sanger/record_loader - revision: 238db7fa24fffee5ad413bd9cd4c6b857d1626c9 - tag: v0.3.0 + revision: 9e7f1b67c3eeee1895f1befc6a54682f11c02135 + tag: v1.1.0 specs: - record_loader (0.3.0) + record_loader (1.1.0) + psych (~> 5.0) GIT remote: https://github.com/sanger/sanger_barcode_format.git @@ -170,7 +171,7 @@ GEM choice (0.2.0) chronic (0.10.2) coderay (1.1.3) - concurrent-ruby (1.3.5) + concurrent-ruby (1.3.6) configatron (4.5.1) connection_pool (2.5.5) crack (1.0.1) @@ -233,7 +234,7 @@ GEM factory_bot_rails (6.5.1) factory_bot (~> 6.5) railties (>= 6.1.0) - faraday (2.14.1) + faraday (2.14.2) faraday-net_http (>= 2.0, < 3.5) json logger @@ -244,14 +245,14 @@ 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.1) + flipper (1.4.2) concurrent-ruby (< 2) - flipper-active_record (1.4.1) + flipper-active_record (1.4.2) activerecord (>= 4.2, < 9) - flipper (~> 1.4.1) - flipper-ui (1.4.1) + flipper (~> 1.4.2) + flipper-ui (1.4.2) erubi (>= 1.0.0, < 2.0.0) - flipper (~> 1.4.1) + flipper (~> 1.4.2) rack (>= 1.4, < 4) rack-protection (>= 1.5.3, < 5.0.0) rack-session (>= 1.0.2, < 3.0.0) @@ -280,16 +281,16 @@ GEM prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.19.4) + json (2.19.5) jsonapi-resources (0.9.0) activerecord (>= 4.1) concurrent-ruby railties (>= 4.1) jsonapi-resources-matchers (1.0.0) jsonapi-resources (>= 0.9.0) - knapsack_pro (9.2.3) - logger + knapsack_pro (10.0.0) rake + thor (~> 1.4) language_server-protocol (3.17.0.5) launchy (3.1.1) addressable (~> 2.8) @@ -378,7 +379,9 @@ GEM pry (>= 0.13, < 0.17) pry-rails (0.3.11) pry (>= 0.13.0) - psych (3.3.4) + psych (5.3.1) + date + stringio public_suffix (7.0.5) puma (7.2.0) nio4r (~> 2.0) @@ -499,8 +502,8 @@ GEM rspec-mocks (>= 3.13.0, < 5.0.0) rspec-support (>= 3.13.0, < 5.0.0) rspec-support (3.13.7) - ruboclean (0.7.1) - rubocop (1.86.1) + ruboclean (0.8.0) + rubocop (1.86.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -552,7 +555,7 @@ GEM ffi (~> 1.12) logger ruby2_keywords (0.0.5) - rubyzip (3.2.2) + rubyzip (3.3.0) sanger-jsonapi-resources (0.1.2) activerecord (>= 4.1) concurrent-ruby @@ -562,7 +565,7 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.16.8) securerandom (0.4.1) - selenium-webdriver (4.43.0) + selenium-webdriver (4.44.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -589,6 +592,7 @@ GEM rbtree set (~> 1.0) ssrf_filter (1.2.0) + stringio (3.2.0) syntax_tree (6.3.0) prettier_print (>= 1.2.0) syntax_tree-haml (4.0.3) @@ -622,7 +626,7 @@ GEM uri (1.1.1) useragent (0.16.11) uuidtools (3.0.0) - vite_rails (3.10.0) + vite_rails (3.11.0) railties (>= 5.1, < 9) vite_ruby (~> 3.0, >= 3.2.2) vite_ruby (3.10.2) @@ -680,7 +684,7 @@ DEPENDENCIES capybara carrierwave caxlsx - concurrent-ruby (= 1.3.5) + concurrent-ruby configatron csv (~> 3.3) cucumber-rails @@ -716,7 +720,6 @@ DEPENDENCIES prettier_print pry-byebug pry-rails - psych (< 4) puma rack-acceptable rack-cors diff --git a/app/controllers/qc_reports_controller.rb b/app/controllers/qc_reports_controller.rb index 28da03c102..15e458aef1 100644 --- a/app/controllers/qc_reports_controller.rb +++ b/app/controllers/qc_reports_controller.rb @@ -45,12 +45,14 @@ def show # rubocop:todo Metrics/AbcSize def create # rubocop:todo Metrics/AbcSize, Metrics/MethodLength study = Study.find_by(id: params[:qc_report][:study_id]) exclude_existing = params[:qc_report][:exclude_existing] == '1' + plate_barcodes = format_plate_barcodes(params[:qc_report][:plate_barcodes_text]) qc_report = QcReport.new( study: study, product_criteria: @product.stock_criteria, exclude_existing: exclude_existing, - plate_purposes: params[:qc_report][:plate_purposes].try(:reject, &:blank?) + plate_purposes: params[:qc_report][:plate_purposes].try(:reject, &:blank?), + plate_barcodes: plate_barcodes ) if qc_report.save @@ -64,6 +66,13 @@ def create # rubocop:todo Metrics/AbcSize, Metrics/MethodLength end end + def format_plate_barcodes(plate_barcodes) + return nil if plate_barcodes.nil? + + # Split the input string into an array of barcodes + plate_barcodes.split(/\s+/) + end + # On form submit of a qc_file. Strictly speaking this should be an update action # on the qc_report itself. However we don't want to force the user to extract # the report identifier from the file. diff --git a/app/models/broadcast_event.rb b/app/models/broadcast_event.rb index 7493d841e6..146c5571ea 100644 --- a/app/models/broadcast_event.rb +++ b/app/models/broadcast_event.rb @@ -19,7 +19,7 @@ class BroadcastEvent < ApplicationRecord # https://api.rubyonrails.org/classes/ActiveRecord/Inheritance/ClassMethods.html validates :sti_type, presence: true - serialize :properties, coder: YAML + serialize :properties, coder: YAML, yaml: { permitted_classes: [ActionController::Parameters] } self.inheritance_column = 'sti_type' broadcast_with_warren diff --git a/app/models/presenters/qc_report_presenter.rb b/app/models/presenters/qc_report_presenter.rb index d9e83a0d44..f549ece1d1 100644 --- a/app/models/presenters/qc_report_presenter.rb +++ b/app/models/presenters/qc_report_presenter.rb @@ -31,7 +31,7 @@ def product_name end def study_name - qc_report.study.name + qc_report.study&.name || 'N/A' end def study_abbreviation diff --git a/app/models/qc_report.rb b/app/models/qc_report.rb index 771890b6f1..317912a24f 100644 --- a/app/models/qc_report.rb +++ b/app/models/qc_report.rb @@ -82,10 +82,12 @@ module ReportBehaviour # You can trigger a synchronous report manually by calling #generate! # rubocop:todo Metrics/MethodLength def generate_report # rubocop:todo Metrics/AbcSize - study.each_well_for_qc_report_in_batches( + Well.qc_report_in_batches( + study, exclude_existing, product_criteria, - (plate_purposes.empty? ? nil : plate_purposes) + (plate_purposes.empty? ? nil : plate_purposes), + (plate_barcodes.empty? ? nil : plate_barcodes) ) do |assets| # If there are some wells of interest, we get them in a list connected_wells = Well.hash_stock_with_targets(assets, product_criteria.target_plate_purposes) @@ -128,6 +130,7 @@ def generate_report # rubocop:todo Metrics/AbcSize has_many :qc_metrics serialize :plate_purposes, type: Array, coder: YAML + serialize :plate_barcodes, type: Array, coder: YAML before_validation :generate_report_identifier, if: :identifier_required? @@ -135,10 +138,22 @@ def generate_report # rubocop:todo Metrics/AbcSize scope :for_report_page, ->(conditions) { order(id: :desc).where(conditions).joins(:product_criteria) } - validates :product_criteria, :study, :state, presence: true + validates :product_criteria, :state, presence: true validates :exclude_existing, inclusion: { in: [true, false], message: 'should be true or false.' } + validate :check_valid_plate_barcodes, if: -> { plate_barcodes.present? } + + # We allow null values for study_id to allow qc_reports to be created without a study (just plate_barcodes) + validates :study, presence: true, unless: -> { plate_barcodes.present? } + + def check_valid_plate_barcodes + invalid_barcodes = plate_barcodes.reject { |barcode| Plate.find_by_barcode(barcode) } + return unless invalid_barcodes.any? + + errors.add(:plate_barcodes, "contain invalid barcodes: #{invalid_barcodes.join(', ')}") + end + # Reports are handled asynchronously def schedule_report Delayed::Job.enqueue QcReportJob.new(id) @@ -166,9 +181,9 @@ def identifier_required? # same product / study abbreviation combo within one second # of each other. def generate_report_identifier - return true if study.nil? || product_criteria.nil? + return true if product_criteria.nil? - rid = [study.abbreviation, product_criteria.product.name, DateTime.now.to_fs(:number)].compact + rid = [study&.abbreviation, product_criteria.product.name, DateTime.now.to_fs(:number)].compact .join('_') .downcase .gsub(/[^\w]/, '_') diff --git a/app/models/request_type/validator.rb b/app/models/request_type/validator.rb index d863ac909f..db32ee1cba 100644 --- a/app/models/request_type/validator.rb +++ b/app/models/request_type/validator.rb @@ -104,7 +104,14 @@ def valid_options belongs_to :request_type, optional: false validates :request_option, :valid_options, presence: true - serialize :valid_options, coder: YAML + serialize :valid_options, coder: YAML, yaml: { + permitted_classes: [ + Range, + RequestType::Validator::ArrayWithDefault, + RequestType::Validator::FlowcellTypeValidator, + RequestType::Validator::LibraryTypeValidator + ] + } delegate :include?, to: :valid_options diff --git a/app/models/study.rb b/app/models/study.rb index 0d2f9127fc..c752cb02a2 100644 --- a/app/models/study.rb +++ b/app/models/study.rb @@ -446,20 +446,6 @@ def validate_ethically_approved false end - def each_well_for_qc_report_in_batches(exclude_existing, product_criteria, plate_purposes = nil) - # @note We include aliquots here, despite the fact they are only needed if we have to set a poor-quality flag - # as in some cases failures are not as rare as you may imagine, and it can cause major performance issues. - base_scope = - Well - .on_plate_purpose_included(PlatePurpose.where(name: plate_purposes || STOCK_PLATE_PURPOSES)) - .for_study_through_aliquot(self) - .without_blank_samples - .includes(:well_attribute, :aliquots, :map, samples: :sample_metadata) - .readonly(true) - scope = exclude_existing ? base_scope.without_report(product_criteria) : base_scope - scope.find_in_batches { |wells| yield wells } - end - def warnings # These studies are now invalid, but the warning should remain until existing studies are fixed. if study_metadata.managed? && study_metadata.data_access_group.blank? diff --git a/app/models/submission/request_options_behaviour.rb b/app/models/submission/request_options_behaviour.rb index f140271a35..24489254f1 100644 --- a/app/models/submission/request_options_behaviour.rb +++ b/app/models/submission/request_options_behaviour.rb @@ -7,7 +7,14 @@ class HashWrapper def self.load(hash_yaml) return hash_yaml if hash_yaml.nil? - YAML.load(hash_yaml) + YAML.safe_load(hash_yaml, + aliases: true, + permitted_classes: [ + ActiveSupport::HashWithIndifferentAccess, + ActiveSupport::TimeWithZone, + ActiveSupport::TimeZone, + Time + ]) end def self.dump(hash) @@ -28,22 +35,20 @@ def request_options=(options) super end + private + def check_request_options check_multipliers_are_valid end - private :check_request_options - # rubocop:todo Metrics/PerceivedComplexity, Metrics/AbcSize - def check_multipliers_are_valid # rubocop:todo Metrics/CyclomaticComplexity + def check_multipliers_are_valid # rubocop:disable Metrics/CyclomaticComplexity multipliers = request_options.try(:[], :multiplier) return if multipliers.blank? # We're ok with nothing being specified! # TODO[xxx]: should probably error if they've specified a request type that isn't being used - errors.add(:request_options, 'negative multiplier supplied') if multipliers.values.map(&:to_i).any?(&:negative?) - errors.add(:request_options, 'zero multiplier supplied') if multipliers.values.map(&:to_i).any?(&:zero?) + multiplier_values = multipliers.values.map(&:to_i) + errors.add(:request_options, 'negative multiplier supplied') if multiplier_values.any?(&:negative?) + errors.add(:request_options, 'zero multiplier supplied') if multiplier_values.any?(&:zero?) false unless errors.empty? end - - # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity - private :check_multipliers_are_valid end diff --git a/app/models/well.rb b/app/models/well.rb index 3cc7b94c57..73eedae295 100644 --- a/app/models/well.rb +++ b/app/models/well.rb @@ -91,14 +91,26 @@ def poly_metadata scope :on_plate_purpose, ->(purposes) { joins(:labware).where(labware: { plate_purpose_id: purposes }) } + scope :on_plate_barcode, + lambda { |*barcodes| + if barcodes.flatten.any? + db_barcodes = Barcode.extract_barcodes(barcodes) + joins(labware: :barcodes).where(barcodes: { barcode: db_barcodes }).distinct + end + } + # added version of scope with includes to avoid multiple calls to LabWhere in qc report when getting storage location # for wells in the same plate scope :on_plate_purpose_included, ->(purposes) do - includes(labware: :barcodes).references(:labware).where(labware: { plate_purpose_id: purposes }) + if purposes.any? + includes(labware: :barcodes).references(:labware).where(labware: { plate_purpose_id: purposes }) + end end - scope :for_study_through_aliquot, ->(study) { joins(:aliquots).where(aliquots: { study_id: study }) } + scope :for_study_through_aliquot, ->(study) { + joins(:aliquots).where(aliquots: { study_id: study }) if study.present? + } scope :with_report, ->(product_criteria) do @@ -369,4 +381,21 @@ def library_name def empty? aliquots.blank? end + + def self.qc_report_in_batches(study, exclude_existing, product_criteria, plate_purposes, plate_barcodes, &) + # @note We include aliquots here, despite the fact they are only needed if we have to set a poor-quality flag + # as in some cases failures are not as rare as you may imagine, and it can cause major performance issues. + # Plate purposes don't need to be specified if plate_barcodes are provided. + # If they are not, then we default to the stock plate purposes if not plate_purposes are provided. + default_plate_purposes = plate_barcodes.present? ? nil : Study::STOCK_PLATE_PURPOSES + base_scope = + for_study_through_aliquot(study) + .on_plate_barcode(plate_barcodes) + .on_plate_purpose_included(PlatePurpose.where(name: plate_purposes || default_plate_purposes)) + .without_blank_samples + .includes(:well_attribute, :aliquots, :map, samples: :sample_metadata) + .readonly(true) + scope = exclude_existing ? base_scope.without_report(product_criteria) : base_scope + scope.find_in_batches(&) + end end diff --git a/app/sequencescape_excel/sequencescape_excel/helpers.rb b/app/sequencescape_excel/sequencescape_excel/helpers.rb index 07c60d4513..0317705694 100644 --- a/app/sequencescape_excel/sequencescape_excel/helpers.rb +++ b/app/sequencescape_excel/sequencescape_excel/helpers.rb @@ -5,7 +5,8 @@ module SequencescapeExcel # Helpers module Helpers def load_file(folder, filename) - YAML.load_file(Rails.root.join(folder, "#{filename}.yml")).with_indifferent_access + file_path = Rails.root.join(folder, "#{filename}.yml") + YAML.safe_load_file(file_path, permitted_classes: [Symbol], aliases: true).with_indifferent_access end end end diff --git a/app/views/qc_reports/_qc_report_form.html.erb b/app/views/qc_reports/_qc_report_form.html.erb index 31ef040db3..a62a97c8c0 100644 --- a/app/views/qc_reports/_qc_report_form.html.erb +++ b/app/views/qc_reports/_qc_report_form.html.erb @@ -1,7 +1,7 @@ <%= form_for(qc_report) do |f| %>
<%= f.label "study_id", 'Study' %> - <%= f.select :study_id, studies, {prompt: 'Select study...', disabled: ['']}, { autocomplete: 'off', required: 'required', class: 'form-control select2' } %> + <%= f.select :study_id, studies, {prompt: 'Select study...', disabled: ['']}, { autocomplete: 'off', class: 'form-control select2' } %>
<%= f.label "product_id", 'Product' %> @@ -11,6 +11,15 @@ <%= f.label "plate_purposes", 'Plate purpose' %> <%= f.select :plate_purposes, plate_purposes, { prompt: 'Select plate purpose...', disabled: [''], selected: 'Stock Plate'}, { autocomplete: 'off', class: 'form-control select2', multiple: 'multiple' } %>
+
+ <%= f.label "plate_barcodes_text", 'Plate barcodes' %> + <%= text_area_tag 'qc_report[plate_barcodes_text]', + params.dig(:qc_report, :plate_barcodes_text), + placeholder: 'SQPP-1234 SQPP-5678 ... ', + cols: 40, + rows: 4, + class: 'form-control' %> +
<%= f.label "exclude_existing", 'Exclude samples with existing reports' %> <%= f.check_box :exclude_existing %> diff --git a/app/views/qc_reports/_qc_reports.html.erb b/app/views/qc_reports/_qc_reports.html.erb index cd454acf1b..689824e6c5 100644 --- a/app/views/qc_reports/_qc_reports.html.erb +++ b/app/views/qc_reports/_qc_reports.html.erb @@ -12,7 +12,7 @@ <% qc_reports.each do |report| %> - <%= report.study.name %> + <%= report.study&.name || 'N/A'%> <%= report.product.name %> <%= report.state.humanize %> <%= report.created_at.to_formatted_s(:sortable) %> diff --git a/app/views/qc_reports/show.html.erb b/app/views/qc_reports/show.html.erb index 614c372819..85c2b40211 100644 --- a/app/views/qc_reports/show.html.erb +++ b/app/views/qc_reports/show.html.erb @@ -1,4 +1,4 @@ -

<%= @report_presenter.product_name %> report for <%= link_to(@report_presenter.study_name,@report_presenter.study) %>

+

<%= @report_presenter.product_name %> report for <%= @report_presenter.study.present? ? link_to(@report_presenter.study_name,@report_presenter.study) : 'N/A' %>

<% @report_presenter.each_header do |attribute,value| %> diff --git a/config/default_records/asset_shapes/default_records.yml b/config/default_records/asset_shapes/default_records.yml index 593842c86c..6e5b3778b5 100644 --- a/config/default_records/asset_shapes/default_records.yml +++ b/config/default_records/asset_shapes/default_records.yml @@ -62,8 +62,8 @@ Fluidigm192: sizes: [192] StripTubeColumn: - horizontal_ratio: 1, - vertical_ratio: 8, + horizontal_ratio: 1 + vertical_ratio: 8 description_strategy: Sequential sizes: [8] diff --git a/config/initializers/accession.rb b/config/initializers/accession.rb index 5602bbdd39..cd061dc4f8 100644 --- a/config/initializers/accession.rb +++ b/config/initializers/accession.rb @@ -9,5 +9,5 @@ end # add ena requirement fields here -ena_requirement_fields = YAML.load_file('config/ena_requirement_fields.yml') +ena_requirement_fields = YAML.safe_load_file('config/ena_requirement_fields.yml') Rails.application.config.ena_requirement_fields = ena_requirement_fields.with_indifferent_access diff --git a/config/initializers/failure_reasons.rb b/config/initializers/failure_reasons.rb index 87c12b48f6..4efde706db 100644 --- a/config/initializers/failure_reasons.rb +++ b/config/initializers/failure_reasons.rb @@ -1,2 +1,2 @@ # frozen_string_literal: true -FAILURE_REASONS = YAML.load(File.open("#{Rails.root}/config/failure_reasons.yml")) +FAILURE_REASONS = YAML.safe_load_file("#{Rails.root}/config/failure_reasons.yml") diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index c64a5c5c81..5d9d87a3b9 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -2,7 +2,7 @@ require 'yaml' require 'flipper/adapters/active_record' -FLIPPER_FEATURES = YAML.load_file('./config/feature_flags.yml') +FLIPPER_FEATURES = YAML.safe_load_file('./config/feature_flags.yml') Rails.application.configure do ## Memoization ensures that only one adapter call is made per feature per request. diff --git a/config/initializers/phi_x.rb b/config/initializers/phi_x.rb index d9c687a077..b4c8cb406e 100644 --- a/config/initializers/phi_x.rb +++ b/config/initializers/phi_x.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true -phi_x_config = YAML.load_file('config/phi_x.yml') +phi_x_config = YAML.safe_load_file('config/phi_x.yml') Rails.application.config.phi_x = phi_x_config.with_indifferent_access diff --git a/config/initializers/process_locale_files_with_erb.rb b/config/initializers/process_locale_files_with_erb.rb index c4eb19e95d..78cc131d13 100644 --- a/config/initializers/process_locale_files_with_erb.rb +++ b/config/initializers/process_locale_files_with_erb.rb @@ -4,7 +4,7 @@ module I18n module Backend module Base def load_yml(filename) - YAML.load(ERB.new(File.read(filename)).result) + YAML.unsafe_load(ERB.new(File.read(filename)).result) end end end diff --git a/config/initializers/psych.rb b/config/initializers/psych.rb index 472b3025db..d4c7384d6e 100644 --- a/config/initializers/psych.rb +++ b/config/initializers/psych.rb @@ -1,22 +1,6 @@ # frozen_string_literal: true Rails.application.configure do - # Fix for Psych::DisallowedClass: Tried to load unspecified class - config.active_record.yaml_column_permitted_classes = - Array(config.active_record.yaml_column_permitted_classes) + - %w[ - Symbol - ActiveSupport::HashWithIndifferentAccess - ActiveSupport::TimeWithZone - ActiveSupport::TimeZone - HashWithIndifferentAccess - RequestType::Validator::ArrayWithDefault - RequestType::Validator::LibraryTypeValidator - RequestType::Validator::FlowcellTypeValidator - ActionController::Parameters - Set - Range - FieldInfo - Time - ] + # Allow YAML columns to contain HashWithIndifferentAccess objects by default + ActiveRecord.yaml_column_permitted_classes += [ActiveSupport::HashWithIndifferentAccess] end diff --git a/db/migrate/20260506135637_add_plate_barcodes_to_qc_reports.rb b/db/migrate/20260506135637_add_plate_barcodes_to_qc_reports.rb new file mode 100644 index 0000000000..af45c4b4c2 --- /dev/null +++ b/db/migrate/20260506135637_add_plate_barcodes_to_qc_reports.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +class AddPlateBarcodesToQcReports < ActiveRecord::Migration[8.0] + def change + add_column :qc_reports, :plate_barcodes, :text, size: :medium + + # Allow null values for study_id to allow qc_reports to be create without a study (just plate_barcodes) + change_column_null :qc_reports, :study_id, true + end +end diff --git a/db/schema.rb b/db/schema.rb index e56c2035b5..b761baed95 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[8.0].define(version: 2026_04_29_114103) do +ActiveRecord::Schema[8.0].define(version: 2026_05_06_135637) 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 @@ -1049,13 +1049,14 @@ create_table "qc_reports", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", options: "ENGINE=InnoDB ROW_FORMAT=DYNAMIC", force: :cascade do |t| t.string "report_identifier", null: false - t.integer "study_id", null: false + t.integer "study_id" t.integer "product_criteria_id", null: false t.boolean "exclude_existing", null: false t.string "state" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.text "plate_purposes", size: :medium + t.text "plate_barcodes", size: :medium t.index ["product_criteria_id"], name: "fk_qc_reports_to_product_criteria" t.index ["report_identifier"], name: "index_qc_reports_on_report_identifier", unique: true t.index ["study_id"], name: "fk_qc_reports_to_studies" diff --git a/db/seeds/0001_snp_plate_purposes.rb b/db/seeds/0001_snp_plate_purposes.rb index 2693e3e51a..9096a9a031 100644 --- a/db/seeds/0001_snp_plate_purposes.rb +++ b/db/seeds/0001_snp_plate_purposes.rb @@ -51,7 +51,7 @@ EOS YAML - .load(plate_purposes) + .safe_load(plate_purposes) .each do |plate_purpose| attributes = plate_purpose.reverse_merge( diff --git a/lib/accession.rb b/lib/accession.rb index 8c27017783..64d98f2365 100644 --- a/lib/accession.rb +++ b/lib/accession.rb @@ -24,7 +24,8 @@ module Accession # @see ftp://ftp.sra.ebi.ac.uk/meta/xsd/ Schema definitions module Helpers def load_file(folder, filename) - YAML.load_file(Rails.root.join(folder, "#{filename}.yml")).with_indifferent_access + file_path = Rails.root.join(folder, "#{filename}.yml") + YAML.safe_load_file(file_path, permitted_classes: [Symbol]).with_indifferent_access end end diff --git a/package.json b/package.json index f3127c1f9d..d6d3ad2c75 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,14 @@ "jsbarcode": "^3.12.3", "jszip": "^3.10.1", "popper.js": "^1.16.1", - "postcss": "^8.5.9", + "postcss": "^8.5.14", "postcss-flexbugs-fixes": "^5.0.2", "postcss-import": "^14.1.0", "postcss-preset-env": "^7.8.3", "sass": "^1.99.0", "select2": "^4.1.0-rc.0", "sortablejs": "^1.15.7", - "terser": "^5.46.2" + "terser": "^5.47.1" }, "devDependencies": { "@vitejs/plugin-legacy": "^8.0", @@ -36,8 +36,8 @@ "jsdom": "^26.0", "prettier": "^3.3", "vite": "^8.0", - "vite-plugin-ruby": "^5.2.0", - "vitest": "^4.1.5" + "vite-plugin-ruby": "^5.2.2", + "vitest": "^4.1.6" }, "_comment": "Required to address https://github.com/npm/cli/issues/4828", "optionalDependencies": { diff --git a/spec/models/bulk_submission_spec.rb b/spec/models/bulk_submission_spec.rb index 68422d4018..6cbde9a8ce 100644 --- a/spec/models/bulk_submission_spec.rb +++ b/spec/models/bulk_submission_spec.rb @@ -57,7 +57,7 @@ create(:project, name: 'Test project') end - context 'a simple submission' do + context 'when creating a simple submission' do let(:submission_template_hash) do { name: 'Illumina-A - Cherrypick for pulldown - Pulldown WGS - HiSeq Paired end sequencing', @@ -93,7 +93,7 @@ end end - context 'an asset driven submission' do + context 'when creating an asset driven submission' do let(:spreadsheet_filename) { 'template_for_bulk_submission.csv' } let!(:asset) { create(:plate, barcode: 'SQPD-1', well_count: 1, well_factory: :untagged_well) } let(:submission_template_hash) do @@ -126,7 +126,7 @@ end end - context 'a submission with PCR cycles' do + context 'when creating a submission with PCR cycles' do let(:spreadsheet_filename) { 'pcr_cycles.csv' } let!(:submission_template) do @@ -157,7 +157,7 @@ end end - context 'a submission with primer_panels' do + context 'when creating a submission with primer_panels' do let(:spreadsheet_filename) { 'primer_panels.csv' } let!(:primer_panel) { create(:primer_panel, name: 'Test panel') } @@ -190,7 +190,7 @@ end end - context 'a submission with bait libraries' do + context 'when creating a submission with bait libraries' do let(:spreadsheet_filename) { '2_valid_sc_submissions.csv' } let!(:bait_library) { create(:bait_library, name: 'Bait library 1') } let!(:bait_library_2) { create(:bait_library, name: 'Bait library 2') } @@ -222,7 +222,7 @@ end end - context 'a submission with a lowercase library type' do + context 'when creating a submission with a lowercase library type' do let(:spreadsheet_filename) { 'with_lowercase_library_type.csv' } let!(:submission_template) do @@ -253,7 +253,7 @@ end end - context 'a submission with an unrecognised library type' do + context 'when creating a submission with an unrecognised library type' do let(:spreadsheet_filename) { 'with_unknown_library_type.csv' } let!(:submission_template) do @@ -274,7 +274,7 @@ end end - context 'a submission with additional template name validations' do + context 'when creating a submission with additional template name validations' do context 'when valid for scRNA template' do let(:submission_template_hash) do { @@ -420,13 +420,13 @@ end context 'when an scRNA Bulk Submission given with invalid number of samples per pool' do - context 'number of samples per pool < 5' do + context 'when number of samples per pool < 5' do let(:spreadsheet_filename) { 'scRNA_bulk_submission_tube_invalid.csv' } it_behaves_like 'an invalid scRNA Bulk Submission', 'scRNA_bulk_submission_tube_invalid', 4 end - context 'number of samples per pool > 25' do + context 'when number of samples per pool > 25' do let(:spreadsheet_filename) { 'scRNA_bulk_submission_tube_invalid_greater.csv' } it_behaves_like 'an invalid scRNA Bulk Submission', 'scRNA_bulk_submission_tube_invalid_greater', 32 @@ -434,7 +434,7 @@ end end - context 'a submission with a NovaSeqX sequencing request type' do + context 'when creating a submission with a NovaSeqX sequencing request type' do let(:spreadsheet_filename) { 'novaseqx_bulk_submission.csv' } let!(:request_types) { [create(:nova_seq_x_sequencing_request_type)] } let(:study) { create(:study, name: 'UAT Study') } @@ -480,7 +480,7 @@ end end - context 'a submission with a NovaSeq 6000 PE sequencing request type' do + context 'when creating a submission with a NovaSeq 6000 PE sequencing request type' do let(:spreadsheet_filename) { 'nova_seq_6000_pe_bulk_submission.csv' } let!(:request_types) { [create(:nova_seq_6000_p_e_sequencing_request_type)] } let(:study) { create(:study, name: 'UAT Study') } diff --git a/spec/models/qc_report_spec.rb b/spec/models/qc_report_spec.rb index fdfd3e9803..17e45b8bca 100644 --- a/spec/models/qc_report_spec.rb +++ b/spec/models/qc_report_spec.rb @@ -3,14 +3,42 @@ require 'rails_helper' RSpec.describe QcReport do - it 'is not valid without a study' do - expect(build(:qc_report, study: nil)).not_to be_valid + context 'study' do + it 'is not valid without a study if plate_barcodes are not present' do + expect(build(:qc_report, study: nil, plate_barcodes: nil)).not_to be_valid + end + + it 'is valid without a study if plate_barcodes are present' do + plate = create(:plate) + report = build(:qc_report, study: nil, plate_barcodes: [plate.human_barcode.to_s]) + expect(report).to be_valid + end end it 'is not valid without a product criteria' do expect(build(:qc_report, product_criteria: nil)).not_to be_valid end + context 'plate_barcodes' do + it 'is not valid if it is present and contains invalid barcodes' do + plate = create(:plate) + report = build(:qc_report, plate_barcodes: [plate.human_barcode.to_s, 'INVALID1', 'INVALID2']) + expect(report).not_to be_valid + expect(report.errors[:plate_barcodes]).to include('contain invalid barcodes: INVALID1, INVALID2') + end + + it 'is valid if all barcodes are valid' do + plates = create_list(:plate, 2) + report = build(:qc_report, plate_barcodes: [plates[0].human_barcode.to_s, plates[1].human_barcode.to_s]) + expect(report).to be_valid + end + + it 'is valid if it is nil and a study is present' do + report = build(:qc_report, plate_barcodes: nil, study: create(:study)) + expect(report).to be_valid + end + end + context 'include existing' do attr_reader :study, :other_study, :stock_plate, :qc_report, :qc_metric_count @@ -199,4 +227,32 @@ expect(qc_report.qc_metrics.count).to eq(3) end end + + context 'limit by plate barcodes' do + attr_reader :qc_report + + let(:study) { create(:study) } + let(:plates) { create_list(:plate, 4, plate_purpose: PlatePurpose.find_or_create_by(name: 'Stock plate')) } + + before do + create(:well_for_qc_report, study: study, plate: plates[0]) + create(:well_for_qc_report, study: study, plate: plates[1]) + create(:well_for_qc_report, study: study, plate: plates[2]) + + @qc_report = + create( + :qc_report, + study: study, + exclude_existing: false, + product_criteria: create(:product_criteria), + plate_purposes: [], + plate_barcodes: plates.map(&:human_barcode) + ) + qc_report.generate! + end + + it 'generates qc_metrics per sample which needs them' do + expect(qc_report.qc_metrics.count).to eq(3) + end + end end diff --git a/spec/models/study_spec.rb b/spec/models/study_spec.rb index 7057d7237f..5c875d158e 100644 --- a/spec/models/study_spec.rb +++ b/spec/models/study_spec.rb @@ -346,40 +346,6 @@ expect(study.name).to eq('Squish double spaces and flanking whitespace but not double letters') end end - - describe '#each_well_for_qc_report_in_batches' do - let!(:study) { create(:study) } - let(:purpose_1) { PlatePurpose.stock_plate_purpose } - let(:purpose_2) { create(:plate_purpose) } - let(:purpose_3) { create(:plate_purpose) } - let(:purpose_4) { create(:plate_purpose) } - let!(:well_1) { create(:well_for_qc_report, study: study, plate: create(:plate, plate_purpose: purpose_1)) } - let!(:well_2) { create(:well_for_qc_report, study: study, plate: create(:plate, plate_purpose: purpose_2)) } - let!(:well_3) { create(:well_for_qc_report, study: study, plate: create(:plate, plate_purpose: purpose_3)) } - let!(:well_4) { create(:well_for_qc_report, study: study, plate: create(:plate, plate_purpose: purpose_4)) } - - it 'will limit by stock plate purposes if there are no plate purposes' do - wells_count = 0 - study.each_well_for_qc_report_in_batches(false, 'Bespoke RNA') { |wells| wells_count += wells.length } - expect(wells_count).to eq(1) - end - - it 'will limit by passed plates purposes' do - wells_count = 0 - study.each_well_for_qc_report_in_batches( - false, - 'Bespoke RNA', - [purpose_2.name, purpose_3.name, purpose_4.name] - ) { |wells| wells_count += wells.length } - expect(wells_count).to eq(3) - - wells_count = 0 - study.each_well_for_qc_report_in_batches(false, 'Bespoke RNA', [purpose_2.name, purpose_3.name]) do |wells| - wells_count += wells.length - end - expect(wells_count).to eq(2) - end - end end describe '#mailing_list_of_managers' do diff --git a/spec/models/well_spec.rb b/spec/models/well_spec.rb index 31e2601abe..aed736920f 100644 --- a/spec/models/well_spec.rb +++ b/spec/models/well_spec.rb @@ -867,4 +867,187 @@ end.to change(Warren.handler.messages, :count).from(0) end end + + describe '.on_plate_purpose_included' do + let(:purpose_1) { create(:plate_purpose) } + let(:purpose_2) { create(:plate_purpose) } + let(:well_1) { create(:well, plate: create(:plate, plate_purpose: purpose_1)) } + let(:well_2) { create(:well, plate: create(:plate, plate_purpose: purpose_2)) } + + it 'returns wells with the included plate purposes' do + expect(described_class.on_plate_purpose_included([purpose_1])).to eq([well_1]) + expect(described_class.on_plate_purpose_included([purpose_2])).to eq([well_2]) + expect(described_class.on_plate_purpose_included([purpose_1, + purpose_2])).to contain_exactly(well_1, well_2) + end + + it 'returns an empty array if no wells have the included plate purposes' do + expect(described_class.on_plate_purpose_included(['Non-existent Purpose'])).to eq([]) + end + + it 'returns an empty array if no purposes are included' do + expect(described_class.on_plate_purpose_included([])).to eq([]) + end + end + + describe '.on_plate_barcode' do + let(:plate_1) { create(:plate) } + let(:plate_2) { create(:plate) } + let(:well_1) { create(:well, plate: plate_1) } + let(:well_2) { create(:well, plate: plate_2) } + + it 'returns wells with the included plate barcodes' do + expect(described_class.on_plate_barcode([plate_1.human_barcode])).to eq([well_1]) + expect(described_class.on_plate_barcode([plate_2.human_barcode])).to eq([well_2]) + expect(described_class.on_plate_barcode([plate_1.human_barcode, + plate_2.human_barcode])).to contain_exactly(well_1, well_2) + end + + it 'returns an empty array if no wells have the included plate barcodes' do + expect(described_class.on_plate_barcode(['Non-existent Barcode'])).to eq([]) + end + end + + describe '.for_study_through_aliquot' do + let(:study) { create(:study) } + let(:aliquot_1) { create(:aliquot, study:) } + let(:aliquot_2) { create(:aliquot, study:) } + let(:well_1) { create(:well, aliquots: [aliquot_1]) } + let(:well_2) { create(:well, aliquots: [aliquot_2]) } + + it 'returns wells for the given study through their aliquots' do + expect(described_class.for_study_through_aliquot(study)).to contain_exactly(well_1, well_2) + end + + it 'returns an empty array if no wells are associated with the given study through their aliquots' do + other_study = create(:study) + expect(described_class.for_study_through_aliquot(other_study)).to eq([]) + end + end + + describe '.without_blank_samples' do + let(:study) { create(:study) } + let(:aliquot_1) { create(:aliquot, study: study, sample: create(:sample, empty_supplier_sample_name: false)) } + let(:aliquot_2) { create(:aliquot, study: study, sample: create(:sample, empty_supplier_sample_name: true)) } + let(:well_1) { create(:well, aliquots: [aliquot_1]) } + let(:well_2) { create(:well, aliquots: [aliquot_2]) } + + it 'returns wells with samples without empty_supplier_sample_names' do + expect(described_class.without_blank_samples).to contain_exactly(well_1) + end + + it 'returns an empty array if no wells contain samples without empty_supplier_sample_names' do + aliquot_2.sample.update(empty_supplier_sample_name: true) + expect(described_class.without_blank_samples).to eq([]) + end + end + + describe '#qc_report_in_batches' do + let(:study) { create(:study) } + let(:purpose_1) { PlatePurpose.stock_plate_purpose } + let(:purpose_2) { create(:plate_purpose) } + let(:purpose_3) { create(:plate_purpose) } + let(:purpose_4) { create(:plate_purpose) } + + before do + create(:well_for_qc_report, study: study, plate: create(:plate, plate_purpose: purpose_1)) + create(:well_for_qc_report, study: study, plate: create(:plate, plate_purpose: purpose_2)) + create(:well_for_qc_report, study: study, plate: create(:plate, plate_purpose: purpose_3)) + create(:well_for_qc_report, study: study, plate: create(:plate, plate_purpose: purpose_4)) + end + + it 'limits by stock plate purposes if there are no plate purposes and no plate barcodes' do + wells_count = 0 + described_class.qc_report_in_batches(study, false, 'Bespoke RNA', nil, nil) do |wells| + wells_count += wells.length + end + expect(wells_count).to eq(1) + end + + it 'does not limit by stock plate purposes if there are no plate purposes but there are plate barcodes' do + plate_1 = create(:plate, plate_purpose: purpose_4) + create(:well_for_qc_report, study: study, plate: plate_1) + + wells_count = 0 + described_class.qc_report_in_batches(study, false, 'Bespoke RNA', nil, [plate_1.human_barcode]) do |wells| + wells_count += wells.length + end + expect(wells_count).to eq(1) + end + + it 'limits by passed plates purposes' do + wells_count = 0 + described_class.qc_report_in_batches( + study, + false, + 'Bespoke RNA', + [purpose_2.name, purpose_3.name, purpose_4.name], + nil + ) { |wells| wells_count += wells.length } + expect(wells_count).to eq(3) + + wells_count = 0 + described_class.qc_report_in_batches(study, false, 'Bespoke RNA', [purpose_2.name, purpose_3.name], + nil) do |wells| + wells_count += wells.length + end + expect(wells_count).to eq(2) + end + + it 'limits by plate barcodes' do + plate_1 = create(:plate, plate_purpose: purpose_1) + plate_2 = create(:plate, plate_purpose: purpose_1) + create(:well_for_qc_report, study: study, plate: plate_1) + create(:well_for_qc_report, study: study, plate: plate_2) + + wells_count = 0 + described_class.qc_report_in_batches(nil, false, 'Bespoke RNA', [purpose_1.name], + [plate_1.human_barcode]) do |wells| + wells_count += wells.length + end + expect(wells_count).to eq(1) + end + + it 'limits by study' do + study_2 = create(:study) + create(:well_for_qc_report, study: study_2, plate: create(:plate, plate_purpose: purpose_1)) + + wells_count = 0 + described_class.qc_report_in_batches(study, false, 'Bespoke RNA', [purpose_1.name], nil) do |wells| + wells_count += wells.length + end + expect(wells_count).to eq(1) + end + + context 'combination of filters' do + it 'limits by study and plate purposes' do + study_2 = create(:study) + create(:well_for_qc_report, study: study_2, plate: create(:plate, plate_purpose: purpose_2)) + + wells_count = 0 + described_class.qc_report_in_batches(study, false, 'Bespoke RNA', [purpose_2.name], nil) do |wells| + wells_count += wells.length + end + expect(wells_count).to eq(1) + end + + it 'limits by study, plate purposes and plate barcodes' do + study_2 = create(:study) + plate_1 = create(:plate, plate_purpose: purpose_2) + plate_2 = create(:plate, plate_purpose: purpose_2) + create(:well_for_qc_report, study: study_2, plate: plate_1) + create(:well_for_qc_report, study: study_2, plate: plate_2) + + wells_count = 0 + described_class.qc_report_in_batches( + study_2, + false, + 'Bespoke RNA', + [purpose_2.name], + [plate_1.human_barcode] + ) { |wells| wells_count += wells.length } + expect(wells_count).to eq(1) + end + end + end end diff --git a/yarn.lock b/yarn.lock index 444336d89b..8d9bbbab53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,11 +22,16 @@ js-tokens "^4.0.0" picocolors "^1.1.1" -"@babel/compat-data@^7.28.6", "@babel/compat-data@^7.29.0": +"@babel/compat-data@^7.28.6": 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/compat-data@^7.29.3": + version "7.29.3" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.3.tgz#e3f5347f0589596c91d227ccb6a541d37fb1307b" + integrity sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg== + "@babel/core@^7.29.0": version "7.29.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" @@ -239,6 +244,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.27.1" +"@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@^7.29.3": + version "7.29.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.3.tgz#2e14f9335803d892ccb67ef487e23cf9726156fe" + integrity sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^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" @@ -479,10 +492,10 @@ "@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== +"@babel/plugin-transform-modules-systemjs@^7.29.4": + version "7.29.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz#f621105da99919c15cf4bde6fcc7346ef95e7b20" + integrity sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w== dependencies: "@babel/helper-module-transforms" "^7.28.6" "@babel/helper-plugin-utils" "^7.28.6" @@ -680,18 +693,19 @@ "@babel/helper-create-regexp-features-plugin" "^7.28.5" "@babel/helper-plugin-utils" "^7.28.6" -"@babel/preset-env@^7.29.2": - 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== +"@babel/preset-env@^7.29.5": + version "7.29.5" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.5.tgz#c48b7ed94582c8b685e21b8b42de8633ec289268" + integrity sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA== dependencies: - "@babel/compat-data" "^7.29.0" + "@babel/compat-data" "^7.29.3" "@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-safari-rest-destructuring-rhs-array" "^7.29.3" "@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" @@ -723,7 +737,7 @@ "@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-systemjs" "^7.29.4" "@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" @@ -1475,17 +1489,17 @@ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@vitejs/plugin-legacy@^8.0": - version "8.0.1" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-legacy/-/plugin-legacy-8.0.1.tgz#3925b78ba9a016654a52552d438e63c112a6ac32" - integrity sha512-8zeDeuNPqXd49rIVgFgluQYB8vQICHR7l+W2I3CxYK4gTjTorajVr0wLvSjALIwEwLRxBn68EgNVyGP4j6hP7w== + version "8.0.2" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-legacy/-/plugin-legacy-8.0.2.tgz#9ed05d941dddb46279d9b95987b1deb112aef34f" + integrity sha512-+n6znc/hTsuD2v/qWX3nfR6UFWFKwpL+XS+SPyiPuEwAvR24iRvLkJQDh18u0XrHPEwuwxPmw0VopvlmLSg66Q== dependencies: "@babel/core" "^7.29.0" "@babel/plugin-transform-dynamic-import" "^7.27.1" - "@babel/plugin-transform-modules-systemjs" "^7.29.0" - "@babel/preset-env" "^7.29.2" + "@babel/plugin-transform-modules-systemjs" "^7.29.4" + "@babel/preset-env" "^7.29.5" babel-plugin-polyfill-corejs3 "^0.14.2" babel-plugin-polyfill-regenerator "^0.6.8" - browserslist "^4.28.1" + browserslist "^4.28.2" browserslist-to-esbuild "^2.1.1" core-js "^3.49.0" magic-string "^0.30.21" @@ -1493,12 +1507,12 @@ systemjs "^6.15.1" "@vitest/coverage-v8@^4.0": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz#26bbdbebecd66be77fa1b63a9ed985dd86a3ba85" - integrity sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A== + version "4.1.7" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-4.1.7.tgz#77059bf103673f44602ddcba1074df58993c3cca" + integrity sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ== dependencies: "@bcoe/v8-coverage" "^1.0.2" - "@vitest/utils" "4.1.5" + "@vitest/utils" "4.1.7" ast-v8-to-istanbul "^1.0.0" istanbul-lib-coverage "^3.2.2" istanbul-lib-report "^3.0.1" @@ -1508,63 +1522,79 @@ std-env "^4.0.0-rc.1" tinyrainbow "^3.1.0" -"@vitest/expect@4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.5.tgz#5caab19535cfb04fbc37087c5608d46e74dc9292" - integrity sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw== +"@vitest/expect@4.1.6": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-4.1.6.tgz#b50c9390aae6957ab4d9e20722cebb17d5bf169a" + integrity sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg== dependencies: "@standard-schema/spec" "^1.1.0" "@types/chai" "^5.2.2" - "@vitest/spy" "4.1.5" - "@vitest/utils" "4.1.5" + "@vitest/spy" "4.1.6" + "@vitest/utils" "4.1.6" chai "^6.2.2" tinyrainbow "^3.1.0" -"@vitest/mocker@4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.5.tgz#9d5791733e4866cfb8af2d48ca371b127e7d2e93" - integrity sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw== +"@vitest/mocker@4.1.6": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-4.1.6.tgz#6b624045745236b02aca879a02aef68b72d9d4cd" + integrity sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ== dependencies: - "@vitest/spy" "4.1.5" + "@vitest/spy" "4.1.6" estree-walker "^3.0.3" magic-string "^0.30.21" -"@vitest/pretty-format@4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.5.tgz#4c13d77a77e2931e44db95522ed5700bcf0570d4" - integrity sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g== +"@vitest/pretty-format@4.1.6": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.6.tgz#24a1c03a6b68a8775f8ddfec51d3636315edc3f5" + integrity sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw== dependencies: tinyrainbow "^3.1.0" -"@vitest/runner@4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.5.tgz#a14dd2d2f48603f906dd52304a10c7fc623bb1de" - integrity sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ== +"@vitest/pretty-format@4.1.7": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-4.1.7.tgz#86b37ea96d4c5bd1357be66982e60a89a343c1bb" + integrity sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw== dependencies: - "@vitest/utils" "4.1.5" + tinyrainbow "^3.1.0" + +"@vitest/runner@4.1.6": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-4.1.6.tgz#b6d189e68bd9927c4f111ad089ff96e4757591b1" + integrity sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA== + dependencies: + "@vitest/utils" "4.1.6" pathe "^2.0.3" -"@vitest/snapshot@4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.5.tgz#d07970d1448190ee5a258db6ab79c65b8018c13b" - integrity sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ== +"@vitest/snapshot@4.1.6": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-4.1.6.tgz#14fdfc8baf6b4b3e4e35763431dbea3aaa8aa0eb" + integrity sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw== dependencies: - "@vitest/pretty-format" "4.1.5" - "@vitest/utils" "4.1.5" + "@vitest/pretty-format" "4.1.6" + "@vitest/utils" "4.1.6" magic-string "^0.30.21" pathe "^2.0.3" -"@vitest/spy@4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.5.tgz#fa7858ffab746fa9ac29496e626f5a0caf9a5a7f" - integrity sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ== +"@vitest/spy@4.1.6": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-4.1.6.tgz#0a316893630f47fa545e33026cfc91575070d165" + integrity sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg== + +"@vitest/utils@4.1.6": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.6.tgz#3f4acf1f60e135ec1ce896f10baa4cd6466d0d38" + integrity sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ== + dependencies: + "@vitest/pretty-format" "4.1.6" + convert-source-map "^2.0.0" + tinyrainbow "^3.1.0" -"@vitest/utils@4.1.5": - version "4.1.5" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.5.tgz#20d6a6ae651a0dd33f945548921698d49701fa43" - integrity sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug== +"@vitest/utils@4.1.7": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-4.1.7.tgz#8d2350588f7054246f24d0dbadffbef5d3a5caff" + integrity sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw== dependencies: - "@vitest/pretty-format" "4.1.5" + "@vitest/pretty-format" "4.1.7" convert-source-map "^2.0.0" tinyrainbow "^3.1.0" @@ -1692,7 +1722,7 @@ browserslist-to-esbuild@^2.1.1: dependencies: meow "^13.0.0" -browserslist@^4.21.4, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.28.1: +browserslist@^4.21.4, browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.28.1, browserslist@^4.28.2: version "4.28.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.2.tgz#f50b65362ef48974ca9f50b3680566d786b811d2" integrity sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg== @@ -3109,7 +3139,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.33, postcss@^8.5.10, postcss@^8.5.9: +postcss@^8.4.33, postcss@^8.5.10: version "8.5.12" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.12.tgz#cd0c0f667f7cb0521e2313234ea6e707a9ec1ddb" integrity sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA== @@ -3118,6 +3148,15 @@ postcss@^8.4.33, postcss@^8.5.10, postcss@^8.5.9: picocolors "^1.1.1" source-map-js "^1.2.1" +postcss@^8.5.14: + version "8.5.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c" + integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -3380,10 +3419,10 @@ systemjs@^6.15.1: resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.15.1.tgz#74175b6810e27a79e1177d21db5f0e3057118cea" integrity sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA== -terser@^5.46.2: - version "5.46.2" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.46.2.tgz#b9529672d5b0024c7959571c83b82f65077b2a4f" - integrity sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw== +terser@^5.47.1: + version "5.47.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.47.1.tgz#99b298e51bc41214304847de1429ec92fd1f7648" + integrity sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.15.0" @@ -3501,10 +3540,10 @@ util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -vite-plugin-ruby@^5.2.0: - 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== +vite-plugin-ruby@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/vite-plugin-ruby/-/vite-plugin-ruby-5.2.2.tgz#3a92ba5d67ebb32f655eee4fb5c79b6995b1bb91" + integrity sha512-q4UY9Bu0pngcbSmrsJBWbzz35Wzx4vAGMPaOIwZLo6TQ6I2n5/x0BL64GXmOxZ4SeDz+t+nDANKTBpp7CFF9AA== dependencies: obug "^2.0" tinyglobby "^0.2.12" @@ -3522,18 +3561,18 @@ vite-plugin-ruby@^5.2.0: optionalDependencies: fsevents "~2.3.3" -vitest@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.5.tgz#cda189c0cd9dd1c920be477c0f371b64ec14782a" - integrity sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg== - dependencies: - "@vitest/expect" "4.1.5" - "@vitest/mocker" "4.1.5" - "@vitest/pretty-format" "4.1.5" - "@vitest/runner" "4.1.5" - "@vitest/snapshot" "4.1.5" - "@vitest/spy" "4.1.5" - "@vitest/utils" "4.1.5" +vitest@^4.1.6: + version "4.1.6" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-4.1.6.tgz#754875c9a09c5a3e8ca7d07d440659d92c19787f" + integrity sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ== + dependencies: + "@vitest/expect" "4.1.6" + "@vitest/mocker" "4.1.6" + "@vitest/pretty-format" "4.1.6" + "@vitest/runner" "4.1.6" + "@vitest/snapshot" "4.1.6" + "@vitest/spy" "4.1.6" + "@vitest/utils" "4.1.6" es-module-lexer "^2.0.0" expect-type "^1.3.0" magic-string "^0.30.21"