sanger/sequencescape

View on GitHub
app/sample_manifest_excel/sample_manifest_excel/upload/processor/plate.rb

Summary

Maintainability
C
7 hrs
Test Coverage
A
100%
# frozen_string_literal: true

require_dependency 'sample_manifest_excel/upload/processor/base'
module SampleManifestExcel
  module Upload
    module Processor
      # TODO: had to explicitly specify the namespace for Base here otherwise it picks up Upload::Base
      ##
      # Processor to handle plate manifest uploads.
      class Plate < SampleManifestExcel::Upload::Processor::Base
        validate :check_for_retention_instruction_by_plate

        # For plate manifests the barcodes (sanger plate id column) should be the same for each well from the same
        # plate, and different for each plate.
        # Uniqueness of foreign barcodes in the database is checked in the specialised field sanger_plate_id.
        def check_for_barcodes_unique
          duplicated_barcode_row, err_msg = duplicate_barcodes
          return if duplicated_barcode_row.nil?

          errors.add(:base, "Barcode duplicated at row: #{duplicated_barcode_row.number}. #{err_msg}")
        end

        # Return the row of the first encountered barcode mismatch
        # rubocop:todo Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
        def duplicate_barcodes # rubocop:todo Metrics/CyclomaticComplexity
          return nil, nil unless upload.respond_to?(:rows)

          unique_bcs = {}
          unique_plates = {}
          upload.rows.each do |row|
            next if row.columns.blank? || row.data.blank?

            plate_barcode = row.value('sanger_plate_id')
            sample_id = row.value('sanger_sample_id')
            next if plate_barcode.nil? || sample_id.nil?

            plate_id_for_sample = find_plate_id_for_sample_id(sample_id)
            next if plate_id_for_sample.nil?

            # Check that a barcode is used for only one plate
            if unique_bcs.key?(plate_barcode)
              err_msg = 'Barcode is used in multiple plates.'
              return row, err_msg unless unique_bcs[plate_barcode] == plate_id_for_sample
            else
              unique_bcs[plate_barcode] = plate_id_for_sample
            end

            # Check that a plate has only one barcode
            if unique_plates.key?(plate_id_for_sample)
              err_msg = 'Plate has multiple barcodes.'
              return row, err_msg unless unique_plates[plate_id_for_sample] == plate_barcode
            else
              unique_plates[plate_id_for_sample] = plate_barcode
            end
          end
          [nil, nil]
        end

        # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity

        def find_plate_id_for_sample_id(sample_id)
          sample_manifest_asset = SampleManifestAsset.find_by(sanger_sample_id: sample_id)
          sample_manifest_asset.labware&.id
        end

        # A subset of the plate manifests (stock plates not library plates) are required to have
        # a retention instruction column to describe how they should be disposed of. The column should
        # have the same value for all manifest rows for the same plate.
        def check_for_retention_instruction_by_plate
          retention_error_row, err_msg = non_matching_retention_instructions_for_plates
          return if retention_error_row.nil?

          errors.add(:base, "Retention instruction checks failed at row: #{retention_error_row.number}. #{err_msg}")
        end

        # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength
        def non_matching_retention_instructions_for_plates
          return nil, nil unless upload.respond_to?(:rows)

          upload
            .rows
            .each_with_object({}) do |row, plate_retentions|
              # ignore empty rows and skip if the retention column is not present
              if row.columns.blank? || row.data.blank? || row.columns.extract(['retention_instruction']).count.zero?
                next
              end

              plate_barcode = row.value('sanger_plate_id')
              sample_id = row.value('sanger_sample_id')

              # ignore rows where primary sample fields have not been filled in
              next unless plate_barcode.present? && sample_id.present?

              # check the row retention instruction is valid
              err_msg = check_row_retention_value(row, plate_barcode, plate_retentions)
              return row, err_msg if err_msg.present?
            end
          [nil, nil]
        end

        # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength

        def check_row_retention_value(row, plate_barcode, plate_retentions)
          # if present the column is mandatory
          row_retention_value = row.value('retention_instruction')
          return 'Value cannot be blank.' if row_retention_value.nil?

          # Check that a plate has only one retention instruction value
          if plate_retentions.key?(plate_barcode)
            if plate_retentions[plate_barcode] != row_retention_value
              return "Plate (#{plate_barcode}) cannot have different retention instruction values."
            end
          else
            # first time we are seeing this plate, add it to plate retentions hash
            plate_retentions[plate_barcode] = row_retention_value
          end
          nil
        end
      end
    end
  end
end