SpeciesFileGroup/taxonworks

View on GitHub
app/models/type_material.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# TypeMaterial links CollectionObjects to Protonyms. It is the single direct relationship between nomenclature (TaxonName) and CollectionObject in TaxonWorks. Other name-collection object relationships coming through TaxonDeterminations, i.e. linking an OTU to a object.
# TypeMaterial is used to encode specific rules of nomenclature, therefor it only includes those types (e.g. "holotype") that are specifically goverened, for example "topotype" is not allowed.
#
# @!attribute protonym_id
#   @return [Integer]
#     the protonym in question
#
# @!attribute collection_object_id
#   @return [Integer]
#     the CollectionObject
#
# @!attribute type_type
#   @return [String]
#     the type of Type relationship (e.g. holotype)
#
# @!attribute project_id
#   @return [Integer
#   the project ID
#
# @!attribute position
#   @return [Integer]
#    sort column
#
class TypeMaterial < ApplicationRecord
  include Housekeeping
  include Shared::Citations
  include Shared::DataAttributes

  include Shared::Notes
  include Shared::Tags
  include Shared::Confidences
  include Shared::IsData
  include SoftValidation

  # Keys are valid values for type_type, values are
  # required Class for BiologicalCollectionObject
  ICZN_TYPES = {
    'holotype' =>  Specimen,
    'paratype' => Specimen,
    'paralectotype' => Specimen,
    'neotype' => Specimen,
    'lectotype' => Specimen,
    'syntype' => Specimen,
    'paratypes' => Lot,
    'syntypes' => Lot,
    'paralectotypes' => Lot
  }.freeze

  ICN_TYPES = {
      'holotype' => Specimen,
      'paratype' => Specimen,
      'lectotype' => Specimen,
      'neotype' => Specimen,
      'epitype' => Specimen,
      'isotype' => Specimen,
      'syntype' => Specimen,
      'isosyntype' => Specimen,
      'syntypes' => Lot,
      'isotypes' => Lot,
      'isosyntypes' => Lot
  }.freeze

  belongs_to :collection_object, class_name: 'CollectionObject', inverse_of: :type_materials
  belongs_to :protonym, inverse_of: :type_materials
  has_many :otus, through: :protonym, inverse_of: :type_materials

  scope :where_protonym, -> (taxon_name) { where(protonym_id: taxon_name) }
  scope :with_type_string, -> (base_string) { where('type_type LIKE ?', "#{base_string}" ) }
  scope :with_type_array, -> (base_array) { where('type_type IN (?)', base_array ) }

  scope :primary, -> {where(type_type: %w{neotype lectotype holotype}).order('collection_object_id')}
  scope :syntypes, -> {where(type_type: %w{syntype syntypes}).order('collection_object_id')}
  scope :primary_with_protonym_array, -> (base_array) {select('type_type, collection_object_id').group('type_type, collection_object_id').where("type_materials.type_type IN ('neotype', 'lectotype', 'holotype', 'syntype', 'syntypes') AND type_materials.protonym_id IN (?)", base_array ) }

  validate :check_type_type
  validate :check_protonym_rank

  validates_uniqueness_of :type_type, scope: [:protonym_id, :collection_object_id]

  soft_validate(:sv_single_primary_type, set: :single_primary_type)
  soft_validate(:sv_type_source, set: :type_source)

  accepts_nested_attributes_for :collection_object, allow_destroy: true
  validates_presence_of :type_type, :protonym, :collection_object

  # TODO: really should be validating uniqueness at this point, it's type material, not garbage records

  def type_source
    [source, protonym.try(:source), nil].compact.first
  end

  def self.legal_type_type(code, type_type)
    case code
    when :iczn
      ICZN_TYPES.keys.include?(type_type)
    when :icn
      ICN_TYPES.keys.include?(type_type)
    else
      false
    end
  end

  protected

  def check_type_type
    if protonym
      code = protonym.rank_class.nomenclatural_code
      errors.add(:type_type, 'Not a legal type for the nomenclatural code provided') unless TypeMaterial::legal_type_type(code, type_type)
    end
  end

  def check_protonym_rank
    errors.add(:protonym_id, 'Type cannot be designated, name is not a species group name') if protonym && !protonym.is_species_rank?
  end

  def sv_single_primary_type
    primary_types = TypeMaterial.with_type_array(['holotype', 'neotype', 'lectotype']).where_protonym(protonym).not_self(self)
    syntypes = TypeMaterial.with_type_array(['syntype', 'syntypes']).where_protonym(protonym)

    if type_type =~ /syntype/
      soft_validations.add(:type_type, 'Other primary types selected for the taxon are conflicting with the syntypes') unless primary_types.empty?
    end

    if ['holotype', 'neotype', 'lectotype'].include?(type_type)
      soft_validations.add(:type_type, 'More than one primary type associated with the taxon') if !primary_types.empty? || !syntypes.empty?
    end
  end

  def sv_type_source
    soft_validations.add(:base, 'Source is not selected neither for type nor for taxon') unless type_source
    if %w(paralectotype neotype lectotype paralectotypes).include?(type_type)
      if source.nil?
        soft_validations.add(:base, "Source for #{type_type} designation is not selected ") if source.nil?
      elsif !protonym.try(:source).nil? && source.cached_nomenclature_date && protonym.cached_nomenclature_date
        soft_validations.add(:base, "#{type_type.capitalize} could not be designated in the original publication") if source == protonym.source
        soft_validations.add(:base, "#{type_type.capitalize} could not be designated before taxon description") if source.cached_nomenclature_date&.to_date < protonym.cached_nomenclature_date
      end
    end
  end

end