SpeciesFileGroup/taxonworks

View on GitHub
app/models/asserted_distribution.rb

Summary

Maintainability
A
0 mins
Test Coverage
# An AssertedDistribution is the Source-backed assertion that a taxon (OTU) is present in some *spatial area*. It requires a Citation indicating where/who made the assertion.
# In TaxonWorks the areas are drawn from GeographicAreas.
#
# AssertedDistributions can be asserts that the source indicates that a taxon is NOT present in an area.  This is a "positive negative" in , i.e. the Source can be thought of recording evidence that a taxon is not present. TaxonWorks does not differentiate between types of negative evidence.
#
# @!attribute otu_id
#   @return [Integer]
#   the OTU ID
#
# @!attribute geographic_area_id
#   @return [Integer]
#   the geographic area ID
#
# @!attribute project_id
#   @return [Integer]
#   the project ID
#
# @!attribute is_absent
#   @return [Boolean]
#     a positive negative, when true then there exists an assertion that the taxon is not present in the spatial area
#
class AssertedDistribution < ApplicationRecord
  include Housekeeping
  include SoftValidation
  include Shared::Notes
  include Shared::Tags
  include Shared::DataAttributes # Why?
  include Shared::CitationRequired # !! must preceed Shared::Citations
  include Shared::Citations
  include Shared::Confidences
  include Shared::OriginRelationship
  include Shared::Identifiers
  include Shared::HasPapertrail
  include Shared::Taxonomy # at present must preceed IsDwcOccurence
  include Shared::IsDwcOccurrence
  include AssertedDistribution::DwcExtensions
  include Shared::IsData

  include Shared::Maps
  include Shared::QueryBatchUpdate

  originates_from 'Specimen', 'Lot', 'FieldOccurrence'

  # @return [Hash]
  #   of known country/state/county values
  attr_accessor :geographic_names

  belongs_to :otu, inverse_of: :asserted_distributions
  has_one :taxon_name, through: :otu
  belongs_to :geographic_area, inverse_of: :asserted_distributions

  has_one :geographic_item, through: :geographic_area, source: :default_geographic_item
  has_many :geographic_items, through: :geographic_area

  validates_presence_of :geographic_area_id, message: 'geographic area is not selected'
  validates :geographic_area, presence: true
  validates :otu, presence: true
  validates_uniqueness_of :geographic_area_id, scope: [:project_id, :otu_id, :is_absent], message: 'this geographic_area, OTU and present/absent combination already exists'
  validate :new_records_include_citation

  # TODO: deprecate scopes referencing single wheres
  scope :with_otu_id, -> (otu_id) { where(otu_id:) }
  scope :with_is_absent, -> { where('is_absent = true') }
  scope :with_geographic_area_array, -> (geographic_area_array) { where('geographic_area_id IN (?)', geographic_area_array) }
  scope :without_is_absent, -> { where('is_absent = false OR is_absent is Null') }

  accepts_nested_attributes_for :otu, allow_destroy: false, reject_if: proc { |attributes| attributes['name'].blank? && attributes['taxon_name_id'].blank? }

  soft_validate(:sv_conflicting_geographic_area, set: :conflicting_geographic_area, name: 'conflicting geographic area', description: 'conflicting geographic area')

  # getter for attr :geographic_names
  def geographic_names
    return @geographic_names if !@geographic_names.nil?
    @geographic_names ||= geographic_area.geographic_name_classification.delete_if{|k,v| v.nil?}
    @geographic_names ||= {}
  end

  # @param [Hash] defaults
  # @return [AssertedDistribution]
  #   used to also stub an #origin_citation, as required
  def self.stub(defaults: {})
    a = AssertedDistribution.new(
      otu_id: defaults[:otu_id],
      origin_citation_attributes: {source_id: defaults[:source_id]})
    a.origin_citation = Citation.new if defaults[:source_id].blank?
    a
  end

  # rubocop:disable Style/StringHashKeys
  # TODO: DRY with helper methods
  # @return [Hash] GeoJSON feature
  def to_geo_json_feature
    retval = {
      'type' => 'Feature',
      'geometry' => RGeo::GeoJSON.encode(self.geographic_area.geographic_items.first.geo_object),
      'properties' => {'asserted_distribution' => {'id' => self.id}}
    }
    retval
  end

  # rubocop:enable Style/StringHashKeys

  # @return [True]
  #   see citable.rb
  def requires_citation?
    true
  end

  def geographic_item
    geographic_area.default_geographic_item
  end

  def has_shape?
    geographic_area.geographic_items.any?
  end

  def self.batch_update(params)
    request = QueryBatchRequest.new(
      async_cutoff: params[:async_cutoff] || 26,
      klass: 'AssertedDistribution',
      object_filter_params: params[:asserted_distribution_query],
      object_params: params[:asserted_distribution],
      preview: params[:preview],
    )

    a = request.filter

    v1 = a.all.distinct.limit(2).pluck(:geographic_area_id).uniq.count
    v2 = a.all.distinct.limit(2).pluck(:otu_id).uniq.count

    cap = 0

    if v1 > 1 && v2 > 1 # many otus, many geographic areas
      cap = 0
      request.cap_reason = 'Records include multiple OTUs *and* multiple geographic areas.'
    elsif v1 > 1
      cap = 0
      request.cap_reason = 'May not update multiple geographic areas to one.' # TODO: revist constraint
    else
      cap = 2000
    end

    request.cap = cap

    query_batch_update(request)
  end

  protected

  # @return [Boolean]
  def new_records_include_citation
    if new_record? && source.blank? && origin_citation.blank? && !citations.any?
      errors.add(:base, 'required citation is not provided')
    end
  end

  # @return [Boolean]
  def sv_conflicting_geographic_area
    unless geographic_area.nil?
      areas = [geographic_area.level0_id, geographic_area.level1_id, geographic_area.level2_id].compact
      if is_absent # this returns an array, not a single GA so test below is not right
        presence = AssertedDistribution
          .without_is_absent
          .with_geographic_area_array(areas)
          .where(otu_id:)
        soft_validations.add(:geographic_area_id, "Taxon is reported as present in #{presence.first.geographic_area.name}") unless presence.empty?
      else
        presence = AssertedDistribution
          .with_is_absent
          .where(otu_id:)
          .with_geographic_area_array(areas)
        soft_validations.add(:geographic_area_id, "Taxon is reported as missing in #{presence.first.geographic_area.name}") unless presence.empty?
      end
    end
  end

  # @param [Hash] options of e.g., {otu_id: 5, source_id: 5, geographic_areas: Array of {GeographicArea}}
  # @return [Array] an array of AssertedDistributions
  def self.stub_new(options = {})
    options.symbolize_keys!
    result = []
    options[:geographic_areas].each do |ga|
      result.push(
        AssertedDistribution.new(
          otu_id: options[:otu_id],
          geographic_area: ga,
          origin_citation_attributes: {source_id: options[:source_id]})
      )
    end
    result
  end
end