sanger/sequencescape

View on GitHub
app/sequencescape_excel/sequencescape_excel/column.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
98%
# frozen_string_literal: true

module SequencescapeExcel
  ##
  # Column creates a particular column with all the information about this column (name, heading,
  # value, type, attribute, should it be locked or unlocked, position of the column,
  # validation, conditional formatting rules)
  # A column is only valid if it has a name and heading.
  class Column
    include Helpers::Attributes

    setup_attributes :name,
                     :updates,
                     :heading,
                     :number,
                     :type,
                     :validation,
                     :value,
                     :unlocked,
                     :conditional_formattings,
                     :attribute,
                     :range,
                     defaults: {
                       number: 0,
                       type: :string,
                       conditional_formattings: {},
                       validation: NullValidation.new
                     }

    validates_presence_of :name, :heading

    delegate :range_name, to: :validation

    # TODO: Because of the way Sample::Metadata is autoloaded we can't check instance_methods.
    # creating a new instance of Sample::Metadata even at startup is incredibly slow.
    # Can't do it as a constant due to Travis failure.
    def self.sample_metadata_model
      @sample_metadata_model ||= Sample::Metadata.new
    end

    def initialize(attributes = {})
      super(default_attributes.merge(attributes))
      self.updates ||= name
    end

    ##
    # If argument is a validation object copy it otherwise
    # create a new validation object
    def validation=(validation)
      return if validation.nil?
      @validation = validation.is_a?(Hash) ? Validation.new(validation) : validation.dup
    end

    ##
    # If argument is a conditional formatting list copy it
    # otherwise create a new conditional formatting list
    def conditional_formattings=(conditional_formattings)
      return if conditional_formattings.nil?

      @conditional_formattings =
        if conditional_formattings.is_a?(Hash)
          ConditionalFormattingList.new(conditional_formattings)
        else
          conditional_formattings.dup
        end
    end

    ##
    # Creates a new Range object.
    def range=(attributes)
      return if attributes.nil?

      @range = attributes.empty? ? NullRange.new : Range.new(attributes)
    end

    ##
    # Some columns need to be unlocked so data can be entered.
    def unlocked?
      unlocked
    end

    def style
      [unlocked? ? :unlocked : :locked, type]
    end

    def metadata_field?
      @metadata_field ||= Column.sample_metadata_model.respond_to?(updates) unless specialised_field?
    end

    def update_metadata(metadata, value)
      metadata.send("#{updates}=", value) if metadata_field?
    end

    def attribute_value(detail)
      detail[attribute] || value
    end

    def specialised_field?
      # We can't use const_defined? here as we want to make sure we trigger rails class loading
      specialised_field.present?
    end

    def specialised_field
      @specialised_field ||= SequencescapeExcel::SpecialisedField.const_get(classify_name, false)
    rescue NameError
      nil
    end

    ##
    # Check whether a column has been updated with all of the references, validations etc.
    def updated?
      @updated
    end

    ##
    # Create a column range based on the first column, first row and last low
    # If the column has a validation range return it or return a NullRange.
    # Update the column validation using the passed worksheet and found range.
    # Update the conditional formatting based on a range and worksheet.
    def update(first_row, last_row, ranges, worksheet)
      self.range = { first_column: number, first_row: first_row, last_row: last_row }

      range = ranges.find_by(range_name) || NullRange.new
      validation.update(range: range, reference: self.range.reference, worksheet: worksheet)

      conditional_formattings.update(
        self.range.references.merge(absolute_reference: range.absolute_reference, worksheet: worksheet)
      )

      @updated = true

      self
    end

    def initialize_dup(source)
      self.range = {}
      self.validation = source.validation
      self.conditional_formattings = source.conditional_formattings
      super
    end

    def self.build_arguments(args, key, conditional_formattings)
      ArgumentBuilder.new(args, key, conditional_formattings).to_h
    end

    # Builds arguments
    class ArgumentBuilder
      attr_reader :arguments

      def initialize(args, key, default_conditional_formattings)
        @arguments = args.merge(name: key)
        combine_conditional_formattings(default_conditional_formattings)
      end

      def to_h
        arguments
      end

      def inspect
        "<#{self.class}: @name=#{name}, @updates=#{updates}, @heading=#{heading}, @number=#{number}, @type=#{type}, " \
          "@validation#{validation}, @value=#{value}, @unlocked=#{unlocked}, " \
          "@conditional_formattings=#{conditional_formattings}, @attribute=#{attribute}, @range=#{range}>"
      end

      private

      def combine_conditional_formattings(defaults)
        return if arguments[:conditional_formattings].blank?

        arguments[:conditional_formattings].each do |k, cf|
          arguments[:conditional_formattings][k] = defaults.find_by(:type, k).combine(cf)
        end
      end
    end

    private

    attr_reader :attribute

    def classify_name
      updates.to_s.delete('?').classify
    end
  end
end