diff --git a/.nvmrc b/.nvmrc index 3fe3b1570a..d845d9d88d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.13.0 +24.14.0 diff --git a/.release-version b/.release-version index d39ebf95de..88927ab769 100644 --- a/.release-version +++ b/.release-version @@ -1 +1 @@ -14.88.0 +14.89.0 diff --git a/Gemfile.lock b/Gemfile.lock index cf0a6863c4..c8ad9a573e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -133,8 +133,8 @@ GEM backports (3.25.2) base64 (0.3.0) benchmark (0.5.0) - bigdecimal (3.3.1) - bootsnap (1.22.0) + bigdecimal (4.0.1) + bootsnap (1.23.0) msgpack (~> 1.2) builder (3.3.0) bullet (8.1.0) @@ -167,7 +167,7 @@ GEM marcel (~> 1.0) nokogiri (~> 1.10, >= 1.10.4) rubyzip (>= 2.4, < 4) - cgi (0.5.0) + cgi (0.5.1) childprocess (5.1.0) logger (~> 1.5) choice (0.2.0) @@ -247,14 +247,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.3.6) + flipper (1.4.0) concurrent-ruby (< 2) - flipper-active_record (1.3.6) + flipper-active_record (1.4.0) activerecord (>= 4.2, < 9) - flipper (~> 1.3.6) - flipper-ui (1.3.6) + flipper (~> 1.4.0) + flipper-ui (1.4.0) erubi (>= 1.0.0, < 2.0.0) - flipper (~> 1.3.6) + flipper (~> 1.4.0) rack (>= 1.4, < 4) rack-protection (>= 1.5.3, < 5.0.0) rack-session (>= 1.0.2, < 3.0.0) @@ -272,14 +272,15 @@ GEM http-accept (1.7.0) http-cookie (1.1.0) domain_name (~> 0.5) - i18n (1.14.7) + i18n (1.14.8) concurrent-ruby (~> 1.0) image_processing (1.14.0) mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) - io-console (0.8.1) - irb (1.15.3) + io-console (0.8.2) + irb (1.17.0) pp (>= 0.6.0) + prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) json (2.18.1) @@ -302,7 +303,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) - loofah (2.24.1) + loofah (2.25.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.9.0) @@ -327,7 +328,7 @@ GEM minitest-profiler (0.0.2) activesupport (>= 4.1.0) minitest (>= 5.3.3) - mocha (3.0.1) + mocha (3.0.2) ruby2_keywords (>= 0.0.5) msgpack (1.8.0) multi_json (1.17.0) @@ -430,8 +431,8 @@ GEM activesupport (>= 4.2) choice (~> 0.2.0) ruby-graphviz (~> 1.2) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) + rails-html-sanitizer (1.7.0) + 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) @@ -454,7 +455,7 @@ GEM rbtree (0.4.6) rdoc (6.3.4.1) regexp_parser (2.11.3) - reline (0.6.2) + reline (0.6.3) io-console (~> 0.5) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) @@ -474,9 +475,9 @@ GEM rspec-mocks (~> 3.13.0) rspec-collection_matchers (1.2.1) rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.13.3) + rspec-core (3.13.6) rspec-support (~> 3.13.0) - rspec-expectations (3.13.3) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-github (3.0.0) @@ -487,10 +488,10 @@ GEM rspec-json_expectations (2.2.0) rspec-longrun (3.1.0) rspec-core (>= 3.5.0, < 4) - rspec-mocks (3.13.2) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.2) + rspec-rails (8.0.3) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) @@ -498,7 +499,7 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.2) + rspec-support (3.13.7) rubocop (1.84.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -542,8 +543,9 @@ GEM rubocop-rspec (~> 3.5) ruby-graphviz (1.2.5) rexml - ruby-prof (1.7.2) + ruby-prof (2.0.2) base64 + ostruct ruby-progressbar (1.13.0) ruby-units (4.1.0) ruby-vips (2.2.3) @@ -560,7 +562,7 @@ GEM crass (~> 1.0.2) nokogiri (>= 1.16.8) securerandom (0.4.1) - selenium-webdriver (4.40.0) + selenium-webdriver (4.41.0) base64 (~> 0.2) logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) @@ -604,7 +606,7 @@ GEM logger temple (0.10.4) test-prof (1.5.2) - thor (1.4.0) + thor (1.5.0) tilt (2.6.1) timecop (0.9.10) timeout (0.4.4) @@ -633,7 +635,7 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.9.1) + webrick (1.9.2) websocket (1.2.11) websocket-driver (0.8.0) base64 @@ -655,7 +657,7 @@ GEM ostruct rainbow yard - zeitwerk (2.7.3) + zeitwerk (2.7.5) PLATFORMS arm64-darwin diff --git a/app/controllers/sample_manifest_upload_with_tag_sequences_controller.rb b/app/controllers/sample_manifest_upload_with_tag_sequences_controller.rb index dfa17a794c..e319f65aa4 100644 --- a/app/controllers/sample_manifest_upload_with_tag_sequences_controller.rb +++ b/app/controllers/sample_manifest_upload_with_tag_sequences_controller.rb @@ -10,7 +10,7 @@ def create return error('No file attached') if params[:upload].blank? if upload_manifest - success('Sample manifest successfully uploaded.') + set_upload_flash_message else error('Your sample manifest couldn\'t be uploaded.') end @@ -27,6 +27,29 @@ def upload_manifest @uploader.run! end + def set_upload_flash_message + warning_rows = rows_with_warnings + return success('Sample manifest successfully uploaded.') if warning_rows.empty? + + apply_warning_flash(warning_rows) + end + + def rows_with_warnings + @uploader.upload.rows.select do |row| + row.respond_to?(:warnings) && row.warnings.any? + end + end + + def apply_warning_flash(rows) + flash[:warning] = { + 'Sample manifest uploaded with warnings!': + rows.flat_map { |row| row.warnings.full_messages }.uniq + } + + redirect_target = (@uploader.study.present? ? sample_manifests_study_path(@uploader.study) : sample_manifests_path) + redirect_to redirect_target + end + def success(message) flash[:notice] = message redirect_target = (@uploader.study.present? ? sample_manifests_study_path(@uploader.study) : sample_manifests_path) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 3f04313817..818ec1d08b 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -115,6 +115,29 @@ def study @submissions = @study.submissions end + def download_scrna_core_pooling_plan # rubocop:disable Metrics/AbcSize,Metrics/MethodLength + begin + submission = Submission.find(params[:id]) + rescue ActiveRecord::RecordNotFound + flash[:error] = "Submission not found with id #{params[:id]}" + redirect_to submissions_path + return + end + + unless submission.scrna_core_cdna_prep_gem_x_5p_submission? + flash[:error] = + 'This submission does not have the correct template for downloading a scRNA Core pooling plan' + redirect_to submission + return + end + + # Generate the pooling plan CSV string using the ScrnaCoreCdnaPrepPoolingPlanGenerator module + csv_string = Submission::ScrnaCoreCdnaPrepPoolingPlanGenerator.generate_pooling_plan(submission) + + send_data csv_string, type: 'text/plain', filename: "#{params[:id]}_scrna_core_pooling_plan.csv", + disposition: 'attachment' + end + ################################################### AJAX ROUTES # TODO[sd9]: These AJAX routes could be re-factored diff --git a/app/frontend/stylesheets/all/sequencescape.scss b/app/frontend/stylesheets/all/sequencescape.scss index 828ca5bfe4..f385b3be1c 100644 --- a/app/frontend/stylesheets/all/sequencescape.scss +++ b/app/frontend/stylesheets/all/sequencescape.scss @@ -149,7 +149,10 @@ h3.card-header-custom { } } -.alert-error ul { +.alert-error ul, +.alert-warning ul, +.alert-notice ul, +.alert-success ul { margin: 0; } @@ -171,6 +174,7 @@ h3.card-header-custom { .alert-cancelled { @extend .alert-warning; } + .text-notice, .text-passed { @extend .text-success; diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 9daa0f8348..67059c53e5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -76,17 +76,37 @@ def render_flashes nil end - # A helper method for render_flashes - If multiple messages, render them as a list, else render as a single div - # @param messages [Array, String] The flash message or messages to be rendered - def render_message(messages) - messages = Array(messages) - if messages.size > 1 - tag.ul { messages.each { |m| concat tag.li(m) } } + # A helper method for render_flashes. + # If messages is a Hash, renders them as a described list, with the keys as the description and + # the values as the items. + # If messages is an Array with multiple messages, renders them as a list. + # If a single message, renders as a single div. + # + # @param messages [Hash, Array, String] The flash message or messages to be rendered + # @return [ActiveSupport::SafeBuffer] HTML-safe string for rendering the messages + def render_message(messages) # rubocop:disable Metrics/MethodLength + case messages + when Hash + safe_join( + messages.map do |description, items| + tag.div(description) + render_in_list(Array(items)) + end + ) + when Array + if messages.size > 1 + render_in_list(messages) + else # messages has only one element, render it as a single div + tag.div(messages.first) + end else - tag.div(messages.first) + tag.div(messages) end end + def render_in_list(messages) + tag.ul { messages.each { |message| concat tag.li(message) } } + end + def api_data { api_version: RELEASE.api_version } end diff --git a/app/models/ability/base_user.rb b/app/models/ability/base_user.rb index 5cec001764..ffb0e74cd9 100644 --- a/app/models/ability/base_user.rb +++ b/app/models/ability/base_user.rb @@ -32,7 +32,7 @@ def grant_privileges # rubocop:todo Metrics/AbcSize, Metrics/MethodLength can %i[read create], Study can :print_asset_group_labels, Study, owners: { id: user.id } can :print_asset_group_labels, Study, managers: { id: user.id } - can %i[read create update edit], Submission + can %i[read create update edit download_scrna_core_pooling_plan], Submission can :read, [TagGroup, TagLayoutTemplate, TagSet] can %i[read update print_swipecard], User, { id: user.id } can %i[projects study_reports], User diff --git a/app/models/multiplexed_library_tube.rb b/app/models/multiplexed_library_tube.rb index 9d5453273b..82ea0fafce 100644 --- a/app/models/multiplexed_library_tube.rb +++ b/app/models/multiplexed_library_tube.rb @@ -18,7 +18,7 @@ def asset_type_for_request_types end def team - creation_requests.first&.product_line + creation_requests&.first&.product_line end def role @@ -41,6 +41,8 @@ def creation_requests direct = requests_as_target.where_is_a(Request::LibraryCreation) return direct unless direct.empty? - parents.includes(:requests_as_target).first.requests_as_target + # Parents should exist but in the case they don't (e.g. asset_links are yet to be created) + # we want to avoid an error and just return an empty array. + parents.includes(:requests_as_target).first&.requests_as_target || [] end end diff --git a/app/models/submission.rb b/app/models/submission.rb index 7d3b6e4e2c..5688c9f945 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -22,6 +22,7 @@ class Submission < ApplicationRecord # rubocop:todo Metrics/ClassLength include Submission::Priorities PER_ORDER_REQUEST_OPTIONS = %w[pre_capture_plex_level gigabases_expected].freeze + SCRNA_CORE_CDNA_PREP_GEM_X_5P = 'Limber-Htp - scRNA Core cDNA Prep GEM-X 5p' self.per_page = 500 @@ -125,6 +126,10 @@ def multiplexed? orders.any?(&:multiplexed?) end + def scrna_core_cdna_prep_gem_x_5p_submission? + orders.first.template_name == SCRNA_CORE_CDNA_PREP_GEM_X_5P + end + # Attempts to find the multiplexed asset (usually a multiplexed library tube) associated # with the submission. Useful when trying to pool requests into a pre-existing tube at the # end of the process. diff --git a/app/models/submission/scrna_core_cdna_prep_pooling_plan_generator.rb b/app/models/submission/scrna_core_cdna_prep_pooling_plan_generator.rb new file mode 100644 index 0000000000..98491c1ae5 --- /dev/null +++ b/app/models/submission/scrna_core_cdna_prep_pooling_plan_generator.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +# +# This module provides a basic pooling plan generator for scRNA core cDNA prep submissions. +# It generates a CSV string which outlines the pooling strategy for the submitted samples +# based on the number of pools and cells per chip well specified in the submission and grouped by study and project. +# The logic for determining the pool layout is aimed to be a mirror of the pooling logic +# used in Limber, specifically in the DonorPoolingCalculator's allocate_wells_to_pools method. +# +# This module also assumes the submission has already been validated with the scrna_core_cdna_prep validators +# to ensure donor clashes and pooling parameters are already checked. +module Submission::ScrnaCoreCdnaPrepPoolingPlanGenerator + # This logic attemps to mirror Limber's pooling logic + # See Limber LabwareCreators::DonorPoolingCalculator (allocate_wells_to_pools) + def self.generate_pooling_plan(submission) + CSV.generate(row_sep: "\r\n") do |csv| + csv << ['Study / Project', 'Pools (num samples)', 'Cells per chip well'] + # It would be nice to refactor the scRNA Validator logic here to pull out the pooling plan logic + grouped_requests(submission).each do |study_project, subgroup| + # Get number_of_pools and cells_per_chip_well requested from the submission + number_of_pools = subgroup.first.request_metadata.number_of_pools + cells_per_chip_well = subgroup.first.request_metadata.cells_per_chip_well + # Build the pools + pools_layout = calculate_pools_layout(subgroup.size, number_of_pools) + + # Join the pool sizes into a string for the CSV output + number_of_samples_in_pool = pools_layout.join(', ') + + csv << [study_project, number_of_samples_in_pool, cells_per_chip_well] + end + end + end + + # This method calculates the layout of pools based on the total number of samples and the number of pools requested. + # It divides the samples as evenly as possible across the pools, and evenly distributes any remainder samples + def self.calculate_pools_layout(number_of_samples, number_of_pools) + # Ideal pool size is just the number of samples divided by the number of pools, but we need to account for + # any remainder if the division isn't exact + ideal_pool_size, remainder = number_of_samples.divmod(number_of_pools) + pools_layout = Array.new(number_of_pools, ideal_pool_size) + remainder.times { |i| pools_layout[i] += 1 } + pools_layout + end + + # Groups the requests associated with a submission by study and project. + def self.grouped_requests(submission) + # Unique by asset to avoid counting the same sample tube multiple times if it appears in multiple requests + # e.g. if a sample tube is requested in two different lanes, we only want to count it once for pooling plan purposes + submission.requests.uniq(&:asset).group_by do |request| + study = request.initial_study.name + project = request.initial_project.name + "#{study} / #{project}" + end + end +end diff --git a/app/sample_manifest_excel/sample_manifest_excel/upload/row.rb b/app/sample_manifest_excel/sample_manifest_excel/upload/row.rb index ccce068b63..f85c9c7643 100644 --- a/app/sample_manifest_excel/sample_manifest_excel/upload/row.rb +++ b/app/sample_manifest_excel/sample_manifest_excel/upload/row.rb @@ -16,12 +16,32 @@ class Row # rubocop:todo Metrics/ClassLength attr_accessor :number, :data, :columns, :cache attr_reader :sanger_sample_id + def warnings + @warnings ||= ActiveModel::Errors.new(self) + end + validates :number, presence: true, numericality: true validate :sanger_sample_id_exists?, if: :sanger_sample_id validates_presence_of :data, :columns validate :country_of_origin_has_correct_case, if: -> { data.present? && columns.present? && columns.names.include?('country_of_origin') } + validate :i7_present + # Ensure i7 column is not blank if it exists in the manifest + def i7_present + return unless columns.present? && data.present? && columns.names.include?('i7') && value('i7').blank? + + warnings.add(:base, "#{row_title} i7 is blank! ") + end + + validate :i5_present + # Ensure i5 column is not blank if it exists in the manifest + def i5_present + return unless columns.present? && data.present? && columns.names.include?('i5') && value('i5').blank? + + warnings.add(:base, "#{row_title} i5 is blank! ") + end + delegate :present?, to: :sample, prefix: true delegate :aliquots, :asset, to: :manifest_asset diff --git a/app/views/sample_manifest_upload_with_tag_sequences/new.html.erb b/app/views/sample_manifest_upload_with_tag_sequences/new.html.erb index 92d1e8d7de..4eaec4ed60 100644 --- a/app/views/sample_manifest_upload_with_tag_sequences/new.html.erb +++ b/app/views/sample_manifest_upload_with_tag_sequences/new.html.erb @@ -1,4 +1,3 @@ - <%= render partial: "side_links" %> <%= page_title "Sample Manifests", "New and upload" %> diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index ebd72980d9..4abdef998b 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -4,6 +4,9 @@ add :menu, "Print labels for #{order.asset_group.name}" => print_study_asset_group_path(order.study,order.asset_group) unless order.asset_group.nil? end %> <%- add :menu, "Submissions Inbox" => submissions_path if can? :read, Submission -%> +<%- add :menu, "Download scRNA Core pooling plan" => download_scrna_core_pooling_plan_submission_path(@presenter.submission.id) if + @presenter.submission.scrna_core_cdna_prep_gem_x_5p_submission? +-%> <%= page_title("Submission", "#{@presenter.submission.id} - #{@presenter.template_name}") %> @@ -70,5 +73,4 @@ end %> <% end %> <% end %> - <%= vite_javascript_tag 'submissions' %> 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.wip.yml new file mode 100644 index 0000000000..cecf8357a5 --- /dev/null +++ b/config/default_records/request_types/025_limber_lcm_triomics_new_added_request_types.wip.yml @@ -0,0 +1,14 @@ +# Request types for LCM Triomics RNASeq +--- +limber_lcm_triomics_rnaseq: + name: LCM Triomics RNASeq + asset_type: Well + order: 1 + request_class_name: IlluminaHtp::Requests::StdLibraryRequest + for_multiplexing: false + billable: true + product_line_name: Short Read + acceptable_purposes: + - LCMT Lysate + library_types: + - Combined LCM RNA 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 new file mode 100644 index 0000000000..824e90cde6 --- /dev/null +++ b/config/default_records/submission_templates/020_lcm_triomics_new_added_submission_templates.wip.yml @@ -0,0 +1,18 @@ +# Submission templates for LCM Triomics EMSeq and RNASeq --- +# LCM Triomics EMSeq submission template +Limber-Htp - LCM Triomics EMSeq: + submission_class_name: "LinearSubmission" + related_records: + 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: + submission_class_name: "LinearSubmission" + related_records: + request_type_keys: ["limber_lcm_triomics_rnaseq"] + product_line_name: LCM Triomics + product_catalogue_name: LCM Triomics + project_name: "UAT Project" diff --git a/config/routes.rb b/config/routes.rb index c9c46204c2..e176e1843a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,23 +2,39 @@ Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html - user_is_admin = ->(req) { User.find_by(id: req.session[:user])&.administrator? } + # Home root to: 'homes#show' - resource :health, only: [:show] resource :home, only: [:show] - resource :phi_x, only: [:show] do - scope module: :phi_x do - resources :stocks - resources :spiked_buffers - end - end + # Health check endpoints + get 'health' => 'health#show', constraints: ->(req) { req.format == :json } # json with stats + get 'health' => 'rails/health#show', as: :rails_health_check # default Rails health check # Error handling endpoints get '/404', to: 'errors#not_found' get '/500', to: 'errors#internal_server_error' get '/503', to: 'errors#service_unavailable' + # Session authentication endpoints + match '/login' => 'sessions#login', :as => :login, :via => %i[get post] + match '/logout' => 'sessions#logout', :as => :logout, :via => %i[get post] + # this is for test only test/functional/authentication_controller_test.rb + # to be removed? + get 'authentication/open' + get 'authentication/restricted' + + # Feature flags + user_is_admin = ->(req) { User.find_by(id: req.session[:user])&.administrator? } + mount Flipper::UI.app => '/flipper', :constraints => user_is_admin + + # Search + resources :searches + resources :lab_searches + + get 'advanced_search' => 'advanced_search#index' + post 'advanced_search/search' => 'advanced_search#search' + + # API v1 mount Api::RootService.new => '/api/1' unless ENV['DISABLE_V1_API'] # @todo Update v2 resources exceptions to reflect resources (e.g., `, except: %i[update]` for `lot`), @@ -141,9 +157,6 @@ end end - match '/login' => 'sessions#login', :as => :login, :via => %i[get post] - match '/logout' => 'sessions#logout', :as => :logout, :via => %i[get post] - resources :plate_summaries, only: %i[index show] do collection { get :search } end @@ -320,6 +333,7 @@ get :study end member do + get :download_scrna_core_pooling_plan post :change_priority post :cancel end @@ -344,8 +358,6 @@ end end - resources :searches - namespace :admin do resources :abilities, only: :index resources :custom_texts @@ -449,12 +461,8 @@ end end - resources :lab_searches resources :events - get 'advanced_search' => 'advanced_search#index' - post 'advanced_search/search' => 'advanced_search#search' - resources :workflows, only: [] do member do # Yes, this is every bit as horrible as it looks. @@ -468,6 +476,13 @@ collection { get :generate_manifest } end + resource :phi_x, only: [:show] do + scope module: :phi_x do + resources :stocks + resources :spiked_buffers + end + end + resources :asset_audits resources :qc_reports, except: %i[delete update] do @@ -627,11 +642,6 @@ resources :location_reports, only: %i[index show create] - # this is for test only test/functional/authentication_controller_test.rb - # to be removed? - get 'authentication/open' - get 'authentication/restricted' - resources :messengers, only: :show # We removed workflows, which broke study links. Some customers may have their own studies bookmarked @@ -646,8 +656,6 @@ end end - mount Flipper::UI.app => '/flipper', :constraints => user_is_admin - # Custom standalone route for bioscan control locations, allowing only # the POST request, migrated from the Lighthouse pickings endpoint. post 'bioscan_control_locations', to: 'bioscan_control_locations#create' diff --git a/config/sample_manifest_excel/columns.yml b/config/sample_manifest_excel/columns.yml index 58be3e02e2..3245f2d206 100644 --- a/config/sample_manifest_excel/columns.yml +++ b/config/sample_manifest_excel/columns.yml @@ -32,28 +32,38 @@ i7: validation: options: type: :textLength - operator: :lessThanOrEqual - formula1: "255" + operator: :between + formula1: "1" + formula2: "255" allowBlank: false showInputMessage: true + showErrorMessage: true promptTitle: "i7" prompt: "Input i7." + errorStyle: :warning + errorTitle: "i7" + error: "i7 cannot be blank" conditional_formattings: - empty_cell: + empty_warning_cell: i5: heading: i5 TAG SEQUENCE unlocked: true validation: options: type: :textLength - operator: :lessThanOrEqual - formula1: "255" + operator: :between + formula1: "1" + formula2: "255" allowBlank: false showInputMessage: true + showErrorMessage: true + errorStyle: :warning + errorTitle: "i5" + error: "i5 cannot be blank" promptTitle: "i5" prompt: "Input i5." conditional_formattings: - empty_cell: + empty_warning_cell: tag_group: heading: TAG GROUP unlocked: true diff --git a/config/sample_manifest_excel/conditional_formattings.yml b/config/sample_manifest_excel/conditional_formattings.yml index 1554673165..a633439a3c 100644 --- a/config/sample_manifest_excel/conditional_formattings.yml +++ b/config/sample_manifest_excel/conditional_formattings.yml @@ -16,6 +16,15 @@ empty_mandatory_cell: formula: "FALSE" operator: :equal priority: 1 +empty_warning_cell: + style: + bg_color: "FF991C" + type: :dxf + options: + type: :cellIs + formula: "FALSE" + operator: :equal + priority: 1 len: style: bg_color: "FF0000" diff --git a/spec/controllers/sample_manifest_upload_with_tag_sequences_controller_spec.rb b/spec/controllers/sample_manifest_upload_with_tag_sequences_controller_spec.rb index f117068b4e..c4815f3947 100644 --- a/spec/controllers/sample_manifest_upload_with_tag_sequences_controller_spec.rb +++ b/spec/controllers/sample_manifest_upload_with_tag_sequences_controller_spec.rb @@ -6,7 +6,7 @@ RSpec.describe SampleManifestUploadWithTagSequencesController, type: :controller do let(:user) { create(:user) } let(:upload_file) { 'pretend-this-is-an-actual-file' } - let(:uploader) { instance_double(SampleManifest::Uploader, run!: true, study: nil) } + let(:uploader) { instance_double(SampleManifest::Uploader, study: nil) } before do allow(controller).to receive(:current_user).and_return(user) @@ -26,8 +26,15 @@ end end - context 'when the file is uploaded successfully' do - before { post :create, params: { upload: upload_file } } + context 'when the file is uploaded successfully without warning' do + before do + allow(controller).to receive(:upload_manifest) do + controller.instance_variable_set(:@uploader, uploader) + true + end + allow(controller).to receive(:rows_with_warnings).and_return([]) + post :create, params: { upload: upload_file } + end it 'sets a success flash message' do expect(flash[:notice]).to eq('Sample manifest successfully uploaded.') @@ -38,6 +45,29 @@ end end + context 'when upload succeeds with warnings' do + let(:warning_row) do + double(warnings: double(full_messages: ['Row 10 warning'])) + end + + before do + allow(controller).to receive(:upload_manifest) do + controller.instance_variable_set(:@uploader, uploader) + true + end + allow(controller).to receive(:rows_with_warnings).and_return([warning_row]) + post :create, params: { upload: upload_file } + end + + it 'sets warning flash message' do + expect(flash[:warning]).to eq({ 'Sample manifest uploaded with warnings!': ['Row 10 warning'] }) + end + + it 'redirects correctly' do + expect(response).to redirect_to(sample_manifests_path) + end + end + context 'when the upload fails due to invalid data' do before do allow(uploader).to receive(:run!).and_raise(AccessionService::AccessionValidationFailed, 'Invalid data') diff --git a/spec/controllers/submissions_controller_spec.rb b/spec/controllers/submissions_controller_spec.rb index 4d15730d21..304c9a9faf 100644 --- a/spec/controllers/submissions_controller_spec.rb +++ b/spec/controllers/submissions_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe SubmissionsController do +RSpec.describe SubmissionsController, type: :controller do render_views let(:request_type) { create(:well_request_type) } @@ -356,6 +356,43 @@ assert_select 'div.alert-danger', 0 end end + + describe '#download_scrna_core_pooling_plan' do + before do + @template = create(:submission_template, name: 'Limber-Htp - scRNA Core cDNA Prep GEM-X 5p') + @study = create(:study, user: @user) + @project = create(:project) + submission_order = create(:order_with_submission, template_name: @template.name, study: @study, + project: @project, + user: @user, + asset_group: create(:asset_group, study: @study)) + @submission = submission_order.submission + end + + it 'downloads a pooling plan' do + get :download_scrna_core_pooling_plan, params: { id: @submission.id } + + expect(response.headers['Content-Disposition']).to include("#{@submission.id}_scrna_core_pooling_plan.csv") + end + + it 'redirects with an error if the submission is not found' do + get :download_scrna_core_pooling_plan, params: { id: 'nonexistent' } + + expect(flash[:error]).to eq('Submission not found with id nonexistent') + expect(response).to redirect_to(submissions_path) + end + + it 'redirects with an error if the submission does not have the correct template' do + @submission.orders.first.update(template_name: 'Some other template') + + get :download_scrna_core_pooling_plan, params: { id: @submission.id } + + expect(flash[:error]).to eq( + 'This submission does not have the correct template for downloading a scRNA Core pooling plan' + ) + expect(response).to redirect_to(submission_path(@submission)) + end + end end def plate_submission(text) diff --git a/spec/data/sample_manifest_excel/columns.yml b/spec/data/sample_manifest_excel/columns.yml index 48a29008c3..83329b9978 100644 --- a/spec/data/sample_manifest_excel/columns.yml +++ b/spec/data/sample_manifest_excel/columns.yml @@ -37,7 +37,7 @@ i7: promptTitle: "i7" prompt: "Input i7." conditional_formattings: - empty_cell: + empty_warning_cell: i5: heading: i5 TAG SEQUENCE unlocked: true @@ -50,6 +50,8 @@ i5: showInputMessage: true promptTitle: "i5" prompt: "i7." + conditional_formattings: + empty_warning_cell: tag_group: heading: TAG GROUP unlocked: true diff --git a/spec/data/sample_manifest_excel/conditional_formattings.yml b/spec/data/sample_manifest_excel/conditional_formattings.yml index 1554673165..a633439a3c 100644 --- a/spec/data/sample_manifest_excel/conditional_formattings.yml +++ b/spec/data/sample_manifest_excel/conditional_formattings.yml @@ -16,6 +16,15 @@ empty_mandatory_cell: formula: "FALSE" operator: :equal priority: 1 +empty_warning_cell: + style: + bg_color: "FF991C" + type: :dxf + options: + type: :cellIs + formula: "FALSE" + operator: :equal + priority: 1 len: style: bg_color: "FF0000" diff --git a/spec/data/sample_manifest_excel/extract/conditional_formattings.yml b/spec/data/sample_manifest_excel/extract/conditional_formattings.yml index 939fb024b0..aa4d6a6930 100644 --- a/spec/data/sample_manifest_excel/extract/conditional_formattings.yml +++ b/spec/data/sample_manifest_excel/extract/conditional_formattings.yml @@ -7,6 +7,15 @@ empty_cell: formula: "FALSE" operator: :equal priority: 1 +empty_warning_cell: + style: + bg_color: "FF991C" + type: :dxf + options: + type: :cellIs + formula: "FALSE" + operator: :equal + priority: 1 len: style: bg_color: "FF0000" diff --git a/spec/features/sample_manifests/uploader_for_manifests_with_tag_sequences_spec.rb b/spec/features/sample_manifests/uploader_for_manifests_with_tag_sequences_spec.rb index a62990cec1..4000cb56b4 100644 --- a/spec/features/sample_manifests/uploader_for_manifests_with_tag_sequences_spec.rb +++ b/spec/features/sample_manifests/uploader_for_manifests_with_tag_sequences_spec.rb @@ -26,6 +26,7 @@ let!(:user) { create(:admin) } let(:columns) { SampleManifestExcel.configuration.columns.tube_library_with_tag_sequences.dup } let(:test_file) { 'test_file.xlsx' } + let(:manifest_type) { 'tube_library_with_tag_sequences' } before do download.save(test_file) @@ -33,7 +34,7 @@ context 'valid' do context 'standard' do - let(:download) { build(:test_download_tubes, columns:) } + let(:download) { build(:test_download_tubes, columns:, manifest_type:) } it 'upload' do login_user(user) @@ -81,7 +82,7 @@ end context 'cgap foreign barcodes' do - let(:download) { build(:test_download_tubes_cgap, columns:) } + let(:download) { build(:test_download_tubes_cgap, columns:, manifest_type:) } it 'upload' do login_user(user) diff --git a/spec/features/submissions/submission_show_spec.rb b/spec/features/submissions/submission_show_spec.rb new file mode 100644 index 0000000000..ef8a05de06 --- /dev/null +++ b/spec/features/submissions/submission_show_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Submission show' do + let(:user) { create(:admin, login: 'user') } + let(:template) { create(:submission_template) } + let(:study) { create(:study, name: 'abc123_study') } + let(:project) { create(:project, name: 'Test project') } + let(:submission) do + order = create(:order_with_submission, template_name: template.name, study: study, project: project, + asset_group: create(:asset_group, study:)) + order.submission + end + + before do + login_user user + visit submission_path(id: submission.id) + end + + describe 'has the correct content' do + it 'shows the submission information' do + expect(page).to have_content("Submission #{submission.id} - #{template.name}") + expect(page).to have_content("Project #{project.name}") + expect(page).to have_content("Study #{study.name}") + end + + it 'shows the correct sidebar links' do + expect(page).to have_link('Print labels for') + expect(page).to have_link('Submissions Inbox') + # This should only be visible for submissions with the correct scRNA template + expect(page).to have_no_link('Download scRNA Core pooling plan') + end + end + + describe 'Limber-Htp - scRNA Core cDNA Prep GEM-X 5p submissions' do + let(:template) { create(:submission_template, name: 'Limber-Htp - scRNA Core cDNA Prep GEM-X 5p') } + + it 'shows the correct sidebar links' do + expect(page).to have_link('Download scRNA Core pooling plan') + end + + it 'downloads the correct pooling plan' do + click_link 'Download scRNA Core pooling plan' + expect(page.response_headers['Content-Disposition']).to include( + "#{submission.id}_scrna_core_pooling_plan.csv" + ) + end + end +end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index a44d79bafb..eda801fb55 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -109,4 +109,55 @@ end end end + + describe '#render_message' do + let(:html) { helper.render_message(messages) } + + context 'when messages is a Hash' do + let(:messages) { { 'Description 1' => ['Item 1', 'Item 2'], 'Description 2' => 'Single Item' } } + + it 'renders each key as a div and each value as a list' do + expect(html).to include('
Description 1
') + .and include('
  • Item 1
  • ') + .and include('
  • Item 2
  • ') + .and include('
    Description 2
    ') + .and include('
  • Single Item
  • ') + end + end + + context 'when messages is an Array with multiple items' do + let(:messages) { ['Error 1', 'Error 2'] } + + it 'renders the messages as a list' do + expect(html).to include('