SpeciesFileGroup/taxonworks

View on GitHub
app/models/collection_object.rb

Summary

Maintainability
A
0 mins
Test Coverage

# A CollectionObject is on or more physical things that have been collected.  Enumerating how many things (@!total) is a task of the curator.
#
# A CollectiongObjects immediate disposition is handled through its relation to containers.  Containers can be nested, labeled, and interally subdivided as necessary.
#
# @!attribute total
#   @return [Integer]
#   The enumerated number of things, as asserted by the person managing the record.  Different totals will default to different subclasses.  How you enumerate your collection objects is up to you.  If you want to call one chunk of coral 50 things, that's fine (total = 50), if you want to call one coral one thing (total = 1) that's fine too.  If not nil then ranged_lot_category_id must be nil.  When =1 the subclass is Specimen, when > 1 the subclass is Lot.
#
# @!attribute type
#   @return [String]
#     the subclass of collection object, e.g. Specimen, Lot, or RangedLot
#
# @!attribute preparation_type_id
#   @return [Integer]
#   How the collection object was prepared.  Draws from a controlled set of values shared by all projects.  For example "slide mounted".  See PreparationType.
#
# @!attribute respository_id
#   @return [Integer]
#   The id of the Repository.  This is an assertion of the "home" repository, i.e. where you would most reasonably find the ColletionObject when it is not "in use" by external parties. Repositories may indicate ownership, but this is inference, not an assetion. There is some notion of "custody" tied to this assertion. The assertion is only that "if this collection object was not being used, then it you can infer that it will be found in this Repository. In the absence of the assertion of a current repository it is reasonable to infer that this is also where the specimen can be currently found, however this inference will not always hold.  See current_repository_id for related issues vs. modeling localization in TaxonWorks and the use of Containers.
#
# @!attribute current_respository_id
#   @return [Integer]
#   The id of the current repository.  The current repository is the Repository that the specimen can be expected to be found at (i.e. "is localized to") at the present time.  See also respository_id.  This is a temporally bound assertion of location of the specimen, not ownership.  In the future this will need to be reconciled with concepts of "custody" (the agent responsible for the specimen) and a stricter modelling of localization (in TaxonWorks this really should be a Container::Collection or Container::Building, i.e. the attribute doesn't really belong here in the long term.
#
# @!attribute project_id
#   @return [Integer]
#   the project ID
#
# @!attribute buffered_collecting_event
#   @return [String]
#   An incoming, typically verbatim, block of data typically as seens as a locality/method/etc. label.  All buffered_ attributes are written but not intended
#   to be deleted or otherwise updated.  Buffered_ attributes are typically only used in rapid data capture, primarily in historical situations.
#
# @!attribute buffered_determinations
#   @return [String]
#   An incoming, typically verbatim, block of data typically as seen a taxonomic determination label.  All buffered_ attributes are written but not intended
#   to be deleted or otherwise updated.  Buffered_ attributes are typically only used in rapid data capture, primarily in historical situations.
#
# @!attribute buffered_other_labels
#   @return [String]
#   An incoming, typically verbatim, block of data, as typically found on label that is unrelated to determinations or collecting events.  All buffered_ attributes are written but not intended to be deleted or otherwise updated.  Buffered_ attributes are typically only used in rapid data capture, primarily in historical situations.
#
# @!attribute ranged_lot_category_id
#   @return [Integer]
#   The id of the user-defined ranged lot category.  See RangedLotCategory.  When present the subclass is "RangedLot".
#
# @!attribute collecting_event_id
#   @return [Integer]
#   The id of the collecting event from whence this object came.  See CollectingEvent.
#
# @!attribute accessioned_at
#   @return [Date]
#   The date when the object was accessioned to the Repository (not necessarily it's current disposition!). If present Repository must be present.
#
# @!attribute deaccession_reason
#   @return [String]
#   A free text explanation of why the object was removed from tracking.
#
# @!attribute deaccessioned_at
#   @return [Date]
#   The date when the object was removed from tracking.  If provide then Repository must be null?! TODO: resolve
#
class CollectionObject < ApplicationRecord
  include GlobalID::Identification
  include Housekeeping

  include Shared::Citations
  include Shared::Containable
  include Shared::DataAttributes
  include Shared::Loanable
  include Shared::Identifiers
  include Shared::Notes
  include Shared::Tags
  include Shared::Depictions
  include Shared::OriginRelationship
  include Shared::Confidences
  include Shared::ProtocolRelationships
  include Shared::HasPapertrail
  include Shared::Observations
  include Shared::IsData
  include Shared::QueryBatchUpdate
  include SoftValidation

  include Shared::BiologicalExtensions

  include Shared::Taxonomy # at present must be before IsDwcOccurence
  include Shared::IsDwcOccurrence
  include CollectionObject::DwcExtensions

  ignore_whitespace_on(:buffered_collecting_event, :buffered_determinations, :buffered_other_labels)

  # TODO: move to export
  CO_OTU_HEADERS = %w{OTU OTU\ name Family Genus Species Country State County Locality Latitude Longitude}.freeze

  BUFFERED_ATTRIBUTES = %i{buffered_collecting_event buffered_determinations buffered_other_labels}.freeze

  GRAPH_ENTRY_POINTS = [:biological_associations, :data_attributes, :taxon_determinations, :biocuration_classifications, :collecting_event, :origin_relationships, :extracts, :observation_matrices]

  # Identifier delegations
  # .catalog_number_cached
  delegate :cached, to: :preferred_catalog_number, prefix: :catalog_number, allow_nil: true
  # .catalog_number_namespace
  delegate :namespace, to: :preferred_catalog_number, prefix: :catalog_number, allow_nil: true

  # CollectingEvent delegations
  delegate :map_center, to: :collecting_event, prefix: :collecting_event, allow_nil: true
  delegate :collectors, to: :collecting_event, prefix: :collecting_event, allow_nil: true

  # Repository delegations
  delegate :acronym, to: :repository, prefix: :repository, allow_nil: true
  delegate :url, to: :repository, prefix: :repository, allow_nil: true

  # Preparation delegations
  delegate :name, to: :preparation_type, prefix: :preparation_type, allow_nil: true

  has_one :accession_provider_role, class_name: 'AccessionProvider', as: :role_object, dependent: :destroy
  has_one :accession_provider, through: :accession_provider_role, source: :person
  has_one :deaccession_recipient_role, class_name: 'DeaccessionRecipient', as: :role_object, dependent: :destroy
  has_one :deaccession_recipient, through: :deaccession_recipient_role, source: :person

  # TODO: Deprecate these models.  Semantics also confuse with origin relationship.
  has_many :derived_collection_objects, inverse_of: :collection_object, dependent: :restrict_with_error
  has_many :collection_object_observations, through: :derived_collection_objects, inverse_of: :collection_objects

  has_many :sqed_depictions, through: :depictions, dependent: :restrict_with_error

  belongs_to :collecting_event, inverse_of: :collection_objects
  belongs_to :preparation_type, inverse_of: :collection_objects
  belongs_to :ranged_lot_category, inverse_of: :ranged_lots
  belongs_to :repository, inverse_of: :collection_objects
  belongs_to :current_repository, class_name: 'Repository', inverse_of: :collection_objects

  has_many :georeferences, through: :collecting_event
  has_many :geographic_items, through: :georeferences

  has_many :collectors, through: :collecting_event

  accepts_nested_attributes_for :collecting_event, allow_destroy: true, reject_if: :reject_collecting_event

  before_validation :assign_type_if_total_or_ranged_lot_category_id_provided

  validates_presence_of :type
  validate :check_that_either_total_or_ranged_lot_category_id_is_present
  validate :check_that_both_of_category_and_total_are_not_present
  validate :collecting_event_belongs_to_project

  soft_validate(
    :sv_missing_accession_fields,
    set: :missing_accession_fields,
    name: 'Missing accession fields',
    description: 'Name or Provider are not selected')

  soft_validate(
    :sv_missing_deaccession_fields,
    set: :missing_deaccession_fields,
    name: 'Missing deaccesson fields',
    description: 'Date, recipient, or reason are not specified')

  scope :with_sequence_name, ->(name) { joins(sequence_join_hack_sql).where(sequences: {name:}) }
  scope :via_descriptor, ->(descriptor) { joins(sequence_join_hack_sql).where(sequences: {id: descriptor.sequences}) }

  has_many :extracts, through: :origin_relationships, source: :new_object, source_type: 'Extract'
  has_many :sequences, through: :extracts

  # This is a hack, maybe related to a Rails 5.1 bug.
  # It returns the SQL that works in 5.0/4.2 that
  # links CollectionObject to Sequences:
  # joins(derived_extracts: [:derived_sequences])
  def self.sequence_join_hack_sql
    %Q{INNER JOIN  "origin_relationships"
               ON  "origin_relationships"."old_object_id" = "collection_objects"."id"
                  AND  "origin_relationships"."new_object_type" = 'Extract'
                  AND  "origin_relationships"."old_object_type" = 'CollectionObject'
       INNER JOIN  "extracts"
               ON  "extracts"."id" =  "origin_relationships"."new_object_id"
       INNER JOIN  "origin_relationships" "origin_relationships_extracts_join"
               ON  "origin_relationships_extracts_join"."old_object_id" = "extracts"."id"
                  AND  "origin_relationships_extracts_join"."new_object_type" = 'Sequence'
                  AND  "origin_relationships_extracts_join"."old_object_type" = 'Extract'
       INNER JOIN  "sequences"
               ON  "sequences"."id" = "origin_relationships_extracts_join"."new_object_id"}
  end

  def self.batch_update(params)
    request = QueryBatchRequest.new(
      async_cutoff: params[:async_cutoff] || 50,
      klass: 'CollectionObject',
      object_filter_params: params[:collection_object_query],
      object_params: params[:collection_object],
      preview: params[:preview],
    )

    request.cap = 1000

    query_batch_update(request)
  end

  def self.batch_update_dwc_occurrence(params)
    q = Queries::CollectionObject::Filter.new(params).all

    r = BatchResponse.new
    r.method = 'batch_update_dwc_occurrence'
    r.klass = 'CollectionObject'

    c = q.all.count

    if c == 0 || c > 10000
      r.cap_reason = 'Too many (or no) collection objects (max 10k)'
      return r
    end

    if c < 51
      q.each do |co|
        co.set_dwc_occurrence
        r.updated.push co.id
      end
    else
      r.async = true
      q.each do |co|
        co.dwc_occurrence_update_query
      end
    end

    return r
  end

  def dwc_occurrence_update_query
    self.send(:set_dwc_occurrence)
  end

  handle_asynchronously :dwc_occurrence_update_query, run_at: Proc.new { 1.second.from_now }, queue: :query_batch_update

  # TODO: move to a helper
  def self.breakdown_status(collection_objects)
    collection_objects = [collection_objects] if collection_objects.class != Array

    breakdown = {
      total_objects:     collection_objects.length,
      collecting_events: {},
      determinations:    {},
      bio_overview:      []
    }

    breakdown.merge!(breakdown_buffered(collection_objects))

    collection_objects.each do |co|
      breakdown[:collecting_events].merge!(co => co.collecting_event) if co.collecting_event
      breakdown[:determinations].merge!(co => co.taxon_determinations) if co.taxon_determinations.load.any?
      breakdown[:bio_overview].push([co.total, co.biocuration_classes.collect { |a| a.name }])
    end

    breakdown
  end

  # @return [Hash]
  #   a unque list of buffered_ values observed in the collection objects passed
  def self.breakdown_buffered(collection_objects)
    collection_objects = [collection_objects] if collection_objects.class != Array
    breakdown = {}
    categories = BUFFERED_ATTRIBUTES

    categories.each do |c|
      breakdown[c] = []
    end

    categories.each do |c|
      collection_objects.each do |co|
        breakdown[c].push co.send(c)
      end
    end

    categories.each do |c|
      breakdown[c].uniq!
    end

    breakdown
  end

  # TODO: this should be refactored to be collection object centric AFTER
  # it is spec'd
  def self.earliest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id:).minimum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id:).minimum(:end_date_year)

    return EARLIEST_DATE if a.nil? && b.nil?  # 1700-01-01

    d = nil

    if a && b
      if a < b
        d = a
      end
    else
      d = a || b
    end
    d.to_s + '-01-01'
  end

  # TODO: this should be refactored to be collection object centric AFTER
  # it is spec'd
  def self.latest_date(project_id)
    a = CollectingEvent.joins(:collection_objects).where(project_id:).maximum(:start_date_year)
    b = CollectingEvent.joins(:collection_objects).where(project_id:).maximum(:end_date_year)

    c = Time.now.strftime('%Y-%m-%d')

    return c if a.nil? && b.nil?

    d = nil

    if a && b
      if a > b
        d = a
      end
    else
      d = a || b
    end

    d.to_s + '/12/31'
  end

  # TODO: Clarify this.
  # CAREFULL - this isn't _in_, this is *with*, if it was in it would be spatial query, not a join(:geographic_items)
  #
  # Find all collection objects which have collecting events which have georeferences which have geographic_items which
  # are located within the geographic item supplied
  # @param [GeographicItem] geographic_item_id
  # @return [Scope] of CollectionObject
  def self.in_geographic_item(geographic_item, limit, steps = false)
    geographic_item_id = geographic_item.id
    if steps
      gi = GeographicItem.find(geographic_item_id)
      # find the geographic_items inside gi
      step_1 = GeographicItem.is_contained_by('any', gi) # .pluck(:id)
      # find the georeferences from the geographic_items
      step_2 = step_1.map(&:georeferences).uniq.flatten
      # find the collecting events connected to the georeferences
      step_3 = step_2.map(&:collecting_event).uniq.flatten
      # find the collection objects associated with the collecting events
      step_4 = step_3.map(&:collection_objects).flatten.map(&:id).uniq
      retval = CollectionObject.where(id: step_4.sort)
    else
      retval = CollectionObject.joins(:geographic_items)
        .where(GeographicItem.contained_by_where_sql(geographic_item.id))
        .limit(limit)
        .includes(:data_attributes, :collecting_event)
    end
    retval
  end

  # TODO: deprecate
  def self.selected_column_names
    @selected_column_names = {
      ce: {in: {}, im: {}},
      co: {in: {}, im: {}},
      bc: {in: {}, im: {}}
    } if @selected_column_names.nil?
    @selected_column_names
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collecting events
  # decode which headers to be displayed for collecting events
  def self.ce_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id:, attribute_subject_type: 'CollectingEvent')
      .distinct
      .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:ce][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id:, attribute_subject_type: 'CollectingEvent')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
        @selected_column_names[:ce][:im][column_name] = {checked: '0'}
      }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.ce_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.collecting_event.internal_attributes
      all_import_das   = collection_object.collecting_event.import_attributes
      group            = collection[:ce]
      unless group.nil?
        group.each_key { |type_key|
          group[type_key.to_sym].each_key { |header|
            this_val = nil
            case type_key.to_sym
            when :in
              all_internal_das.each { |da|
                if da.predicate.name == header
                  this_val = da.value
                  break
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            when :im
              all_import_das.each { |da|
                if da.import_predicate == header
                  this_val = da.value
                  break
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            else
            end
          }
        }
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for collection objects
  # decode which headers to be displayed for collection objects
  def self.co_headers(project_id)
    CollectionObject.selected_column_names
    cvt_list = InternalAttribute.where(project_id:, attribute_subject_type: 'CollectionObject')
      .distinct
      .pluck(:controlled_vocabulary_term_id)
    # add selectable column names (unselected) to the column name list list
    ControlledVocabularyTerm.where(id: cvt_list).map(&:name).sort.each { |column_name|
      @selected_column_names[:co][:in][column_name] = {checked: '0'}
    }
    ImportAttribute.where(project_id:, attribute_subject_type: 'CollectionObject')
      .pluck(:import_predicate).uniq.sort.each { |column_name|
        @selected_column_names[:co][:im][column_name] = {checked: '0'}
      }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.co_attributes(collection_object, col_defs)
    retval = []; collection = col_defs
    unless collection.nil?
      # for this collection object, gather all the possible data_attributes
      all_internal_das = collection_object.internal_attributes
      all_import_das   = collection_object.import_attributes
      group            = collection[:co]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].each_key { |header|
              this_val = nil
              all_internal_das.each { |da|
                if da.predicate.name == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
        unless group.empty?
          unless group[:im].empty?
            group[:im].each_key { |header|
              this_val = nil
              all_import_das.each { |da|
                if da.import_predicate == header
                  this_val = da.value
                end
              }
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Integer] project_id
  # @return [Hash] of column names and types for biocuration classifications
  # decode which headers to be displayed for biocuration classifications
  def self.bc_headers(project_id)
    CollectionObject.selected_column_names
    # add selectable column names (unselected) to the column name list list
    BiocurationClass.where(project_id:).map(&:name).each { |column_name|
      @selected_column_names[:bc][:in][column_name] = {checked: '0'}
    }
    @selected_column_names
  end

  # @param [CollectionObject] collection_object from which to extract attributes
  # @param [Hash] col_defs - collection of selected headers, prefixes, and types
  # @return [Array] of attributes
  # Retrieve all the attributes associated with the column names (col_defs) for a specific collection_object
  def self.bc_attributes(collection_object, col_defs)
    retval = []
    collection = col_defs
    unless collection.nil?
      group = collection[:bc]
      unless group.nil?
        unless group.empty?
          unless group[:in].empty?
            group[:in].each_key { |header|
              this_val = collection_object.biocuration_classes.map(&:name).include?(header) ? '1' : '0'
              retval.push(this_val) # push one value (nil or not) for each selected header
            }
          end
        end
      end
    end
    retval
  end

  # @param [Array] collecting_event_ids (e.g., from CollectingEvent.in_date_range)
  # @param [Array] area_object_ids (e.g., from GeographicItem.gather_selected_data())
  # @return [Scope] of intersection of collecting events (usually by date range)
  #   and collection objects (usually by inclusion in geographic areas/items)
  def self.from_collecting_events(collecting_event_ids, area_object_ids, area_set, project_id)
    collecting_events_clause = {collecting_event_id: collecting_event_ids, project: project_id}
    area_objects_clause      = {id: area_object_ids, project: project_id}

    if (collecting_event_ids.empty?)
      collecting_events_clause = {project: project_id}
    end

    if (area_object_ids.empty?)
      area_objects_clause = {}
      if (area_set)
        area_objects_clause = 'false'
      end
    end

    retval = CollectionObject.joins(:collecting_event)
      .where(collecting_events_clause)
      .where(area_objects_clause)
    retval
  end

  # TODO: move to filter
  # @param [Hash] search_start_date string in form 'yyyy-mm-dd'
  # @param [Hash] search_end_date string in form 'yyyy-mm-dd'
  # @param [Hash] partial_overlap 'on' or 'off'
  # @return [Scope] of selected collection objects through collecting events with georeferences, remember to scope to project!
  def self.in_date_range(search_start_date: nil, search_end_date: nil, partial_overlap: 'on')
    allow_partial = (partial_overlap.downcase == 'off' ? false : true) # TODO: Just get the correct values from the form!
    q = Queries::CollectingEvent::Filter.new(start_date: search_start_date, end_date: search_end_date, partial_overlap_dates: allow_partial)
    joins(:collecting_event).where(q.between_date_range_facet.to_sql)
  end

  # @param used_on [String] required, one of `TaxonDetermination`, `BiologicalAssociation`
  # @return [Scope]
  #    the max 10 most recently used collection_objects, as `used_on`
  def self.used_recently(user_id, project_id, used_on = '')
    return [] if used_on != 'TaxonDetermination' && used_on != 'BiologicalAssociation'
    t = case used_on
        when 'TaxonDetermination'
          TaxonDetermination.arel_table
        when 'BiologicalAssociation'
          BiologicalAssociation.arel_table
        end

    p = CollectionObject.arel_table

    # i is a select manager
    i = case used_on
        when 'BiologicalAssociation'
          t.project(t['biological_association_subject_id'], t['updated_at']).from(t)
            .where(
              t['updated_at'].gt(1.week.ago).and(
                t['biological_association_subject_type'].eq('CollectionObject')
              )
            )
              .where(t['updated_by_id'].eq(user_id))
              .where(t['project_id'].eq(project_id))
              .order(t['updated_at'].desc)
        else
          # TODO: update to reference new TaxonDetermination
          t.project(t['taxon_determination_object_id'], t['taxon_determination_object_type'], t['updated_at']).from(t)
            .where(t['updated_at'].gt( 1.week.ago ))
            .where(t['updated_by_id'].eq(user_id))
            .where(t['project_id'].eq(project_id))
            .order(t['updated_at'].desc)
        end

    # z is a table alias
    z = i.as('recent_t')

    j = case used_on
        when 'BiologicalAssociation'
          Arel::Nodes::InnerJoin.new(z, Arel::Nodes::On.new(
            z['biological_association_subject_id'].eq(p['id'])
          ))
        else
          # TODO: needs to be fixed to scope the taxon_determination_object_type
          Arel::Nodes::InnerJoin.new(z, Arel::Nodes::On.new(z['taxon_determination_object_id'].eq(p['id'])))
        end

    CollectionObject.joins(j).pluck(:id).uniq
  end

  # @params target [String] one of `TaxonDetermination`, `BiologicalAssociation` , nil
  # @return [Hash] otus optimized for user selection
  def self.select_optimized(user_id, project_id, target = nil)
    r = used_recently(user_id, project_id, target)
    h = {
      quick: [],
      pinboard: CollectionObject.pinned_by(user_id).where(project_id:).to_a,
      recent: []
    }

    if target && !r.empty?
      n = target.tableize.to_sym
      h[:recent] = CollectionObject.where('"collection_objects"."id" IN (?)', r.first(10) ).to_a
      h[:quick] = (CollectionObject.pinned_by(user_id).pinboard_inserted.where(project_id:).to_a  +
                   CollectionObject.where('"collection_objects"."id" IN (?)', r.first(4) ).to_a).uniq
    else
      h[:recent] = CollectionObject.where(project_id:, updated_by_id: user_id).order('updated_at DESC').limit(10).to_a
      h[:quick] = CollectionObject.pinned_by(user_id).pinboard_inserted.where(project_id:).to_a
    end

    h
  end

  # @return [Identifier::Local::CatalogNumber, nil]
  #   the first (position) catalog number for this collection object, either on specimen, or container
  def preferred_catalog_number
    if i = Identifier::Local::CatalogNumber.where(identifier_object: self).order(:position).first
      i
    else
      if container
        container.identifiers.where(identifiers: {type: 'Identifier::Local::CatalogNumber'}).order(:position).first
      else
        nil
      end
    end
  end

  def geographic_name_classification
    # don't load the whole object, just the fields we need
    if a = DwcOccurrence.where(dwc_occurrence_object: self).select(:country, :stateProvince, :county).first

      c = a.country
      s = a.stateProvince
      y = a.county

      v = ::Utilities::Geo::DICTIONARY[c]
      c = v if v
      # s = v if v = ::Utilities::Geo::DICTIONARY[s] # None in there yet
      # y = v if v = ::Utilities::Geo::DICTIONARY[y] # None in there yet

      return {
        country: c,
        state: s,
        county: y
      }
    end
  end

  # return [Boolean]
  #    True if instance is a subclass of BiologicalCollectionObject
  def is_biological?
    self.class <= BiologicalCollectionObject ? true : false
  end

  def annotations
    h = annotations_hash
    (h['biocuration classifications'] = biocuration_classes) if is_biological? && biocuration_classifications.load.any?
    h
  end

  def sv_missing_accession_fields
    soft_validations.add(:accessioned_at, 'Date is not selected') if self.accessioned_at.nil? && !self.accession_provider.nil?
    soft_validations.add(:base, 'Provider is not selected') if !self.accessioned_at.nil? && self.accession_provider.nil?
  end

  def sv_missing_deaccession_fields
    soft_validations.add(:deaccessioned_at, 'Date is not selected') if self.deaccessioned_at.nil? && self.deaccession_reason.present?
    soft_validations.add(:base, 'Recipient is not selected') if self.deaccession_recipient.nil? && self.deaccession_reason && self.deaccessioned_at
    soft_validations.add(:deaccession_reason, 'Reason is is not defined') if self.deaccession_reason.blank? && self.deaccession_recipient && self.deaccessioned_at
  end

  def sv_missing_determination
    # see biological_collection_object
  end

  def sv_missing_collecting_event
    # see biological_collection_object
  end

  def sv_missing_preparation_type
    # see biological_collection_object
  end

  def sv_missing_repository
    # WHY? -  see biological_collection_object
  end

  def sv_missing_biocuration_classification
    # see biological_collection_object
  end

  # See Depiction#destroy_image_stub_collection_object
  # Used to determin if the CO can be
  # destroy after moving an image off
  # this object.
  def is_image_stub?
    r = [
      collecting_event_id.blank?,
      !depictions.reload.any?,
      identifiers.count <= 1,
      !taxon_determinations.any?,
      !type_materials.any?,
      !citations.any?,
      !data_attributes.any?,
      !notes.any?,
      !observations.any?
    ]

   !r.include?(false)

  end

  protected

  def collecting_event_belongs_to_project
    if collecting_event&.persisted? && (Current.project_id || project_id)
      errors.add(:base, 'collecting event is not from this project') if collecting_event.project_id != (Current.project_id || project_id)
    end
  end

  def check_that_both_of_category_and_total_are_not_present
    errors.add(:ranged_lot_category_id, 'Both ranged_lot_category and total can not be set') if ranged_lot_category_id.present? && total.present?
  end

  def check_that_either_total_or_ranged_lot_category_id_is_present
    errors.add(:base, 'Either total or a ranged lot category must be provided') if ranged_lot_category_id.blank? && total.blank?
  end

  def assign_type_if_total_or_ranged_lot_category_id_provided
    if self.total == 1
      self.type = 'Specimen'
    elsif self.total.to_i > 1
      self.type = 'Lot'
    elsif total.nil? && ranged_lot_category_id.present?
      self.type = 'RangedLot'
    end
    true
  end

  def reject_collecting_event(attributed)
    reject = true
    CollectingEvent.core_attributes.each do |a|
      if attributed[a].present?
        reject = false
        break
      end
    end
    # !! does not account for georeferences_attributes!
    reject
  end

end

require_dependency 'specimen'
require_dependency 'lot'
require_dependency 'ranged_lot'