app/models/taxon_name_classification.rb
require_dependency Rails.root.to_s + '/app/models/nomenclatural_rank.rb'
require_dependency Rails.root.to_s + '/app/models/taxon_name_relationship.rb'
# A {https://github.com/SpeciesFileGroup/nomen NOMEN} derived classfication (roughly, a status) for a {TaxonName}.
#
# @!attribute taxon_name_id
# @return [Integer]
# the id of the TaxonName being classified
#
# @!attribute type
# @return [String]
# the type of classifiction (Rails STI)
#
# @!attribute project_id
# @return [Integer]
# the project ID
#
class TaxonNameClassification < ApplicationRecord
include Housekeeping
include Shared::Citations
include Shared::Notes
include Shared::IsData
include SoftValidation
belongs_to :taxon_name, inverse_of: :taxon_name_classifications
before_validation :validate_taxon_name_classification
before_validation :validate_uniqueness_of_latinized
validates_presence_of :taxon_name
validates_presence_of :type
validates_uniqueness_of :taxon_name_id, scope: [:type, :project_id]
validate :nomenclature_code_matches
scope :where_taxon_name, -> (taxon_name) {where(taxon_name_id: taxon_name)}
scope :with_type_string, -> (base_string) {where('taxon_name_classifications.type LIKE ?', "#{base_string}" ) }
scope :with_type_base, -> (base_string) {where('taxon_name_classifications.type LIKE ?', "#{base_string}%" ) }
scope :with_type_array, -> (base_array) {where('taxon_name_classifications.type IN (?)', base_array ) }
scope :with_type_contains, -> (base_string) {where('taxon_name_classifications.type LIKE ?', "%#{base_string}%" ) }
soft_validate(:sv_proper_classification,
set: :proper_classification,
fix: :sv_fix_proper_classification,
name: 'Applicable status',
description: 'Check the status applicability.' )
soft_validate(:sv_proper_year,
set: :proper_classification,
name: 'Applicable protonym year',
description: 'Check that the status is compatible with the year of publication of taxon.' )
soft_validate(:sv_validate_disjoint_classes,
set: :validate_disjoint_classes,
name: 'Conflicting status',
description: 'Taxon has two conflicting statuses' )
soft_validate(:sv_not_specific_classes,
set: :not_specific_classes,
name: 'Not specific status',
description: 'More specific statuses are preffered, for example: "Nomen nudum, no description" is better than "Nomen nudum".' )
after_save :set_cached
after_destroy :set_cached
def nomenclature_code
return :iczn if type.match(/::Iczn/)
return :icnp if type.match(/::Icnp/)
return :icvcn if type.match(/::Icvcn/)
return :icn if type.match(/::Icn/)
return nil
end
def self.label
name.demodulize.underscore.humanize.downcase.gsub(/\d+/, ' \0 ').squish
end
# @return class
# this method calls Module#module_parent
def self.parent
self.module_parent
end
# @return [String]
# the class name, "validated" against the known list of names
def type_name
r = self.type.to_s
::TAXON_NAME_CLASSIFICATION_NAMES.include?(r) ? r : nil
end
def type_class=(value)
write_attribute(:type, value.to_s)
end
def type_class
r = read_attribute(:type).to_s
r = ::TAXON_NAME_CLASSIFICATION_NAMES.include?(r) ? r.safe_constantize : nil
end
# @return [String]
# a humanized class name, with code appended to differentiate
# !! explored idea of LABEL in individual subclasses, use this if this doesn't work
# this is helper-esqe, but also useful in validation, so here for now
def classification_label
return nil if type_name.nil?
type_name.demodulize.underscore.humanize.downcase.gsub(/\d+/, ' \0 ').squish #+
#(nomenclature_code ? " [#{nomenclature_code}]" : '')
end
# @return [String]
# the NOMEN id for this classification
def nomen_id
self.class::NOMEN_URI.split('/').last
end
# Attributes can be overridden in descendants
# @return [Integer]
# the minimum year of applicability for this class, defaults to 1
def self.code_applicability_start_year
1
end
# @return [Integer]
# the last year of applicability for this class, defaults to 9999
def self.code_applicability_end_year
9999
end
# @return [Array of Strings of NomenclaturalRank names]
# nomenclatural ranks to which this class is applicable, that is, only {TaxonName}s of these {NomenclaturalRank}s may be classified as this class
def self.applicable_ranks
[]
end
# @return [Array of Strings of TaxonNameClassification names]
# the disjoint (inapplicable) {TaxonNameClassification}s for this class, that is, {TaxonName}s classified as this class can not be additionally classified under these classes
def self.disjoint_taxon_name_classes
[]
end
# @return [String, nil]
# if applicable, a DWC gbif status for this class
def self.gbif_status
nil
end
def self.assignable
false
end
#def self.common
# false
#end
# @todo Perhaps not inherit these three meaxonNameClassificationsHelper::descendants_collection( TaxonNameClassification::Latinized )thods?
# @return [Array of Strings]
# the possible suffixes for a {TaxonName} name (species) classified as this class, for example see {TaxonNameClassification::Latinized::Gender::Masculine}
# used to validate gender agreement of species name with a genus
def self.possible_species_endings
[]
end
# @return [Array of Strings]
# the questionable suffixes for a {TaxonName} name classified as this class, for example see {TaxonNameClassification::Latinized::Gender::Masculine}
def self.questionable_species_endings
[]
end
# @return [Array of Strings]
# the possible suffixes for a {TaxonName} name (genus) classified as this class, for example see {TaxonNameClassification::Latinized::Gender::Masculine}
def self.possible_genus_endings
[]
end
def self.nomen_uri
const_defined?(:NOMEN_URI, false) ? self::NOMEN_URI : nil
end
def set_cached
set_cached_names_for_taxon_names
end
# TODO: move these to individual classes?!
def set_cached_names_for_taxon_names
begin
TaxonName.transaction_with_retry do
t = taxon_name
if type_name =~ /(Fossil|Hybrid|Candidatus)/
n = t.get_full_name
t.update_columns(
cached: n,
cached_html: t.get_full_name_html(n),
cached_original_combination: t.get_original_combination,
cached_original_combination_html: t.get_original_combination_html
)
elsif type_name =~ /Latinized::PartOfSpeech/
n = t.get_full_name
t.update_columns(
cached: n,
cached_html: t.get_full_name_html(n),
cached_original_combination: t.get_original_combination,
cached_original_combination_html: t.get_original_combination_html
)
TaxonNameRelationship::OriginalCombination.where(subject_taxon_name: t).collect{|i| i.object_taxon_name}.uniq.each do |t1|
t1.update_cached_original_combinations
end
TaxonNameRelationship::Combination.where(subject_taxon_name: t).collect{|i| i.object_taxon_name}.uniq.each do |t1|
t1.update_column(:verbatim_name, t1.cached) if t1.verbatim_name.nil?
n = t1.get_full_name
t1.update_columns(
cached: n,
cached_html: t1.get_full_name_html(n)
)
end
elsif type_name =~ /Latinized::Gender/
t.descendants.with_same_cached_valid_id.each do |t1|
n = t1.get_full_name
t1.update_columns(
cached: n,
cached_html: t1.get_full_name_html(n)
)
end
TaxonNameRelationship::OriginalCombination.where(subject_taxon_name: t).collect{|i| i.object_taxon_name}.uniq.each do |t1|
t1.update_cached_original_combinations
end
TaxonNameRelationship::Combination.where(subject_taxon_name: t).collect{|i| i.object_taxon_name}.uniq.each do |t1|
t1.update_column(:verbatim_name, t1.cached) if t1.verbatim_name.nil?
n = t1.get_full_name
t1.update_columns(
cached: n,
cached_html: t1.get_full_name_html(n)
)
end
elsif TAXON_NAME_CLASS_NAMES_VALID.include?(type_name)
# TaxonName.where(cached_valid_taxon_name_id: t.cached_valid_taxon_name_id).each do |vn|
# vn.update_column(:cached_valid_taxon_name_id, vn.get_valid_taxon_name.id) # update self too!
# end
vn = t.get_valid_taxon_name
vn.update_columns(
cached_valid_taxon_name_id: vn.id,
cached_is_valid: !vn.unavailable_or_invalid?) # Do not change!
vn.list_of_invalid_taxon_names.each do |s|
s.update_columns(
cached_valid_taxon_name_id: vn.id,
cached_is_valid: false)
s.combination_list_self.each do |c|
c.update_columns(cached_valid_taxon_name_id: vn.id)
end
end
t.combination_list_self.each do |c|
c.update_columns(cached_valid_taxon_name_id: vn.id)
end
else
t.update_columns(cached_is_valid: false)
end
end
rescue ActiveRecord::RecordInvalid
false
end
true
end
#region Validation
def validate_uniqueness_of_latinized
true # moved to subclasses
end
#endregion
#region Soft validation
def sv_proper_classification
if TAXON_NAME_CLASSIFICATION_NAMES.include?(self.type)
# self.type_class is a Class
if not self.type_class.applicable_ranks.include?(self.taxon_name.rank_string)
soft_validations.add(:type, "The status '#{self.classification_label}' is not applicable to the taxon #{self.taxon_name.cached_html} at the rank of #{self.taxon_name.rank_class.rank_name}", success_message: 'The status was deleted', failure_message: 'Fail to delete the status')
end
end
end
def sv_fix_proper_classification
begin
TaxonNameClassification.transaction do
self.destroy
end
return true
rescue
return false
end
end
def sv_proper_year
y = self.taxon_name.year_of_publication
if !y.nil? && (y > self.type_class.code_applicability_end_year || y < self.type_class.code_applicability_start_year)
soft_validations.add(:type, "The status '#{self.classification_label}' is not applicable to the taxon #{self.taxon_name.cached_html} published in the year #{y}")
end
end
def sv_validate_disjoint_classes
classifications = TaxonNameClassification.where_taxon_name(self.taxon_name).not_self(self)
classifications.each do |i|
soft_validations.add(:type, "The status '#{self.classification_label}' conflicting with another status: '#{i.classification_label}'") if self.type_class.disjoint_taxon_name_classes.include?(i.type_name)
end
end
def sv_not_specific_classes
true # moved to subclasses
end
#endregion
def self.annotates?
true
end
def annotated_object
taxon_name
end
@@subclasses_preloaded = false
def self.descendants
unless @@subclasses_preloaded
Dir.glob("#{Rails.root}/app/models/taxon_name_classification/**/*.rb")
.sort { |a, b| a.split('/').count <=> b.split('/').count }
.map { |p| p.split('/app/models/').last.sub(/\.rb$/, '') }
.map { |p| p.split('/') }
.map { |c| c.map { |n| ActiveSupport::Inflector.camelize(n) } }
.map { |c| c.join('::') }.map(&:constantize)
@@subclasses_preloaded = true
end
super
end
private
def nomenclature_code_matches
if taxon_name && type && nomenclature_code
tn = taxon_name.type == 'Combination' ? taxon_name.protonyms.last : taxon_name
nc = tn.rank_class.nomenclatural_code
errors.add(:taxon_name, "#{taxon_name.cached_html} belongs to #{taxon_name.rank_class.nomenclatural_code} nomenclatural code, but the status used from #{nomenclature_code} nomenclature code") if nomenclature_code != nc
end
end
# TODO: unnecessary! Type handling will raise here
def validate_taxon_name_classification
errors.add(:type, 'Status not found') if !self.type.nil? and !TAXON_NAME_CLASSIFICATION_NAMES.include?(self.type.to_s)
end
# @todo move these to a shared library (see NomenclaturalRank too)
def self.collect_to_s(*args)
args.collect{|arg| arg.to_s}
end
# @todo move these to a shared library (see NomenclaturalRank too)
# !! using this strongly suggests something can be optimized, meomized etc.
def self.collect_descendants_to_s(*classes)
ans = []
classes.each do |klass|
ans += klass.descendants.collect{|k| k.to_s}
end
ans
end
# @todo move these to a shared library (see NomenclaturalRank too)
# !! using this strongly suggests something can be optimized, meomized etc.
def self.collect_descendants_and_itself_to_s(*classes)
classes.collect{|k| k.to_s} + self.collect_descendants_to_s(*classes)
end
end
#Dir[Rails.root.to_s + '/app/models/taxon_name_classification/**/*.rb'].each { |file| require_dependency file }