sanger/sequencescape

View on GitHub
app/sample_manifest_excel/sample_manifest_excel/upload/row.rb

Summary

Maintainability
A
3 hrs
Test Coverage
A
98%
# frozen_string_literal: true

module SampleManifestExcel
  module Upload
    ##
    # A Row relates to a row in a sample manifest spreadsheet.
    # Each Row relates to a sample
    # Required fields:
    # *number: Number of the row which is used for error tracking
    # *data: An array of sample data
    # *columns: The columns which relate to the data.
    class Row # rubocop:todo Metrics/ClassLength
      include ActiveModel::Model
      include Converters

      attr_accessor :number, :data, :columns, :cache
      attr_reader :sanger_sample_id

      validates :number, presence: true, numericality: true
      validate :sanger_sample_id_exists?, if: :sanger_sample_id
      validates_presence_of :data, :columns

      delegate :present?, to: :sample, prefix: true
      delegate :aliquots, :asset, to: :manifest_asset

      ##
      # Finds a sample based on the sanger_sample_id column. Must exist for row to be valid.
      # Creates the specialised fields for updating the sample based on the passed columns
      def initialize(attributes = {})
        super
        @cache ||= SampleManifestAsset
        @sanger_sample_id ||= value(:sanger_sample_id).presence if columns.present? && data.present?
      end

      ##
      # Finds the data value for a particular column.
      # Offset by 1. Columns have numbers data is an array
      def at(col_num)
        val = data[col_num - 1]
        strip_all_blanks(val)
      end

      ##
      # Find a value based on a column name
      def value(key)
        column_number = columns.find_column_or_null(:name, key).number

        # column_number is -1 if no column found by this name (returns NullColumn object from find)
        return nil if column_number.negative?
        at(column_number)
      end

      def first?
        number == 1
      end

      ##
      # Used for errors
      def row_title
        "Row #{number} -"
      end

      def aliquot
        @aliquot ||= manifest_asset.aliquot
      end
      deprecate aliquot: 'Chromium manifests may have multiple aliquots. Please use aliquots instead.'

      def metadata
        @metadata ||= sample.sample_metadata
      end

      def specialised_fields
        @specialised_fields ||= create_specialised_fields
      end

      ##
      # Updating the sample involves:
      # *Checking it is ok to update row
      # *Updating all of the specialised fields in the aliquot
      # *Updating the sample metadata
      # *Saving the asset, metadata and sample
      # rubocop:todo Metrics/MethodLength
      def update_sample(tag_group, override) # rubocop:todo Metrics/AbcSize
        return unless valid?

        @reuploaded = sample.updated_by_manifest

        if sample.updated_by_manifest && !override
          @sample_skipped = true
        else
          update_specialised_fields(tag_group)
          asset.save!
          update_metadata_fields
          metadata.save!
          sample.updated_by_manifest = true
          sample.empty_supplier_sample_name = false
          @sample_updated = sample.save
        end
      end

      # rubocop:enable Metrics/MethodLength

      def changed?
        (@sample_updated && sample.previous_changes.present?) || metadata.previous_changes.present? ||
          aliquots.any? { |a| a.previous_changes.present? }
      end

      def update_specialised_fields(tag_group)
        specialised_fields.each { |specialised_field| specialised_field.update(tag_group: tag_group) }
      end

      def update_metadata_fields
        columns.with_metadata_fields.each do |column|
          value = at(column.number)
          column.update_metadata(metadata, value) if value.present?
        end
      end

      ##
      # If it is a multiplexed library tube the aliquot is transferred
      # from the library tube to a multiplexed library tube and stated set to passed.
      def transfer_aliquot
        return unless valid?

        asset.external_library_creation_requests.each do |request|
          @aliquot_transferred = request.passed? || request.manifest_processed!
        end
      end

      def reuploaded?
        @reuploaded || false
      end

      def sample
        @sample ||= manifest_asset&.find_or_create_sample if sanger_sample_id.present? && !empty?
      end

      def sample_updated?
        @sample_updated || false
      end

      def sample_skipped_or_updated?
        @sample_skipped || sample_updated?
      end

      def sample_created?
        sample_updated? && !reuploaded?
      end

      def aliquot_transferred?
        @aliquot_transferred
      end

      def empty?
        # a row must have one of the primary column options
        primary_column_names = %w[supplier_name bioscan_supplier_name]

        # check the columns exist, are valid, and at least one of the primary column options are present
        unless columns.present? && columns.valid? &&
                 (primary_column_names.any? { |column_name| columns.names.include? column_name })
          return true
        end

        # it is mandatory to have a value in the primary column
        return true if primary_column_names.all? { |column_name| value(column_name).blank? }
        false
      end

      def labware
        sample.primary_receptacle.labware
      end

      def validate_sample
        check_sample_present
        sample_can_be_updated
        errors.empty?
      end

      private

      def manifest_asset
        @manifest_asset ||= cache.find_by(sanger_sample_id: sanger_sample_id)
      end

      def sanger_sample_id_exists?
        return false if manifest_asset.present?

        errors.add(:base, "#{row_title} Cannot find sample manifest for Sanger ID: #{sanger_sample_id}")
      end

      def sample_can_be_updated
        return unless errors.empty?

        check_primary_receptacle
        check_specialised_fields
        check_sample_metadata
      end

      def check_primary_receptacle
        return if sample.primary_receptacle.present?

        errors.add(:base, "#{row_title} Does not have a primary receptacle.")
      end

      def check_specialised_fields
        return unless errors.empty?

        specialised_fields.each do |specialised_field|
          unless specialised_field.valid?
            errors.add(:base, "#{row_title} #{specialised_field.errors.full_messages.join(', ')}")
          end
        end
      end

      def check_sample_metadata
        # it has to be called here, otherwise metadata errors will not appear
        update_metadata_fields
        return if metadata.valid?

        errors.add(:base, "#{row_title} #{metadata.errors.full_messages.join(', ')}")
      end

      def check_sample_present
        return if sample_present?

        errors.add(:base, "#{row_title} Sample can't be blank.")
      end

      def create_specialised_fields
        return [] unless columns.present? && data.present? && sanger_sample_id.present?

        specialised_fields =
          columns.with_specialised_fields.map do |column|
            column.specialised_field.new(value: at(column.number), sample_manifest_asset: manifest_asset)
          end

        specialised_fields.tap { |fields| link_tag_groups_and_indexes(fields) }
      end

      # link fields together for tag groups and indexes
      def link_tag_groups_and_indexes(fields)
        indexed_fields = fields.index_by(&:class)
        fields.each { |field| field.link(indexed_fields) }
      end
    end
  end
end