Class: Combination
- Inherits:
-
TaxonName
- Object
- ActiveRecord::Base
- ApplicationRecord
- TaxonName
- Combination
- Defined in:
- app/models/combination.rb
Overview
A nomenclator name, composed of existing Protonyms. Each record reflects the subsequent use of two or more protonyms. Only the first use of a combination is stored here, subsequence uses of this combination are referenced in Citations.
A Combination has no name, it exists to group related Protonyms into an epithet.
They are applicable to genus group names and finer epithets.
All elements of the combination must be defined, nothing is assumed based on the relationship to the parent.
c = Combination.new
c.genus = a_protonym_genus
c.species = a_protonym_species
c.save # => true
c.genus_taxon_name_relationship # => A instance of TaxonNameRelationship::Combination::Genus
# or
c = Combination.new(genus: genus_protonym, species: species_protonym)
Getters and setters for each of the APPLICABLE_RANKS are available:
`genus subgenus section subsection series subseries species subspecies variety subvariety form subform`
`genus_id subgenus_id section_id subsection_id series_id subseries_id species_id subspecies_id variety_id subvariety_id form_id subform_id`
You can do things like (notice mix/match of _id or not):
c = Combination.new(genus_id: @genus_protonym.id, subspecies: @some_species_group)
c.species_id = Protonym.find(some_species_id).id
or
c.species = Protonym.find(some_species_id)
Combinations are composed of TaxonNameRelationships. In those relationship the Combination#id is always the `object_taxon_name_id`, the individual Protonyms are stored in `subject_taxon_name_id`.
Constant Summary collapse
- APPLICABLE_RANKS =
The ranks that can be used to build combinations. ! TODO: family group names ?
%w{family subfamily tribe subtribe genus subgenus section subsection series subseries species subspecies variety subvariety form subform}.freeze
Constants inherited from TaxonName
TaxonName::ALTERNATE_VALUES_FOR, TaxonName::COMBINATION_ELEMENTS, TaxonName::NOMEN_VALID, TaxonName::NOT_LATIN, TaxonName::NO_CACHED_MESSAGE, TaxonName::SPECIES_EPITHET_RANKS
Constants included from SoftValidation
SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS
Instance Attribute Summary collapse
-
#combination_verbatim_name ⇒ String
Use with caution, and sparingly! If the combination of values from Protonyms can not reflect the formulation of the combination as provided by the original author that string can be provided here.
-
#disable_combination_relationship_check ⇒ Object
TODO: make access private.
-
#parent_id ⇒ Integer
the parent is the parent of the highest ranked component Protonym, it is automatically set i.e.
Attributes inherited from TaxonName
#also_create_otu, #cached_classified_as, #foo, #no_cached
Class Method Summary collapse
- .find_by_protonym_ids(**keyword_args) ⇒ Scope
- .match_exists?(name = nil, **keyword_args) ⇒ Combination, false
-
.matching_protonyms(name = nil, **protonym_ids) ⇒ Protonym Scope
Hmmm- a Protonym class method?! Protonyms matching original relations, if name provided then name added as an additional check on verbatim match.
- .protonyms_matching_original_relationships(protonym_ids = {}) ⇒ Object
Instance Method Summary collapse
-
#combination_class_relationships(rank_string) ⇒ Object
return [Array of TaxonNameRelationship] classes that are applicable to this name, as deterimned by Rank.
-
#combination_relationships_and_stubs(rank_string) ⇒ Object
TODO: DEPRECATE this is likely not required in our new interfaces.
- #composition ⇒ Object protected
- #does_not_exist_as_original_combination ⇒ Object protected
-
#earliest_protonym_year ⇒ Integer?
The earliest year (nomenclature) that a component Protonym was published on.
- #finest_protonym ⇒ Object
-
#full_name_hash ⇒ Hash
Overrides TaxonName#full_name_hash.
- #get_author_and_year ⇒ Object
- #get_valid_taxon_name ⇒ Object
-
#is_current_placement? ⇒ Boolean
True if the finest level (typically species) currently has the same parent.
- #is_unique ⇒ Object protected
-
#parent_is_properly_set ⇒ Object
protected
The parent of a Combination is the parent of the highest ranked protonym in that Combination.
-
#protonym_ids_params ⇒ Hash
Like `{ genus: 1, species: 2 }`.
-
#protonyms ⇒ Array of TaxonName
TODO: hard code sort order.
-
#protonyms_by_association ⇒ Array of TaxonNames?
Return the component names for this combination prior to it being saved.
-
#protonyms_by_rank ⇒ Hash of {rank: Protonym}?
The component names for this combination prior to it being saved (used to return values prior to save).
-
#publication_years ⇒ Array of Integers
The collective years the protonyms were (nomenclaturaly) published on (ordered from genus to below).
- #set_parent ⇒ Object protected
- #sv_combination_duplicates ⇒ Object protected
- #sv_source_not_older_than_protonyms ⇒ Object protected
-
#sv_year_of_publication_matches_source ⇒ Object
protected
TODO: this is a TaxonName level validation, it doesn't belong here.
- #sv_year_of_publication_not_older_than_protonyms ⇒ Object protected
- #validate_absence_of_subject_relationships ⇒ Object protected
Methods inherited from TaxonName
#all_taxon_name_relationships, #ancestor_at_rank, #ancestor_hash, #ancestor_protonyms, #ancestors_through_parents, #author_string, #cached_author, #cached_html_name_and_author_year, #cached_name_and_author_year, #cached_year, #check_for_children, #check_new_parent_class, #check_new_rank_class, #classification_invalid_or_unavailable?, #classification_valid?, #clear_cached, #combination_list_all, #combination_list_self, #combined_statuses, #create_otu, #descendant_protonyms, #descendants_at_rank, #first_possible_valid_taxon_name, #first_possible_valid_taxon_name_relationship, foo, #full_name_array, #gbif_status_array, #gender_class, #gender_instance, #gender_name, #genderized_name, #get_cached_classified_as, #get_cached_misspelling, #get_full_name, #get_full_name_html, #get_genus_species, #get_original_combination, #get_original_combination_html, #icn_author_and_year, #iczn_author_and_year, #is_candidatus?, #is_combination?, #is_fossil?, #is_genus_or_species_rank?, #is_hybrid?, #is_italicized?, #is_protonym?, #is_valid?, #list_of_invalid_taxon_names, #matrix_row_item, #name_in_gender, #name_is_misapplied?, #name_with_misspelling, #next_sibling, #nomenclature_date, #normalized_genus, #not_binomial?, not_leaves, #original_author_year, parent, #parent_is_set?, #part_of_speech_class, #part_of_speech_instance, #part_of_speech_name, #previous_sibling, #rank, #rank_class, #rank_class=, #rank_string, #reified_id, #related_taxon_names, #relationship_invalid?, #safe_self_and_ancestors, select_optimized, #set_cached, #set_cached_author_year, #set_cached_classified_as, #set_cached_valid_taxon_name_id, #set_cached_warnings, sort_by_rank, #statuses_from_classifications, #statuses_from_relationships, #sv_cached_names, #sv_conflicting_subordinate_taxa, #sv_fix_cached_names, #sv_fix_missing_author, #sv_fix_missing_year, #sv_fix_parent_is_valid_name, #sv_homotypic_synonyms, #sv_hybrid_name_relationships, #sv_incomplete_combination, #sv_missing_author, #sv_missing_classifications, #sv_missing_confidence_level, #sv_missing_etymology, #sv_missing_original_publication, #sv_missing_relationships, #sv_missing_year, #sv_not_synonym_of_self, #sv_parent_is_valid_name, #sv_parent_priority, #sv_potential_homonyms, #sv_primary_types, #sv_single_sub_taxon, #sv_species_gender_agreement, #sv_two_unresolved_alternative_synonyms, #sv_type_placement, #sv_validate_coordinated_names, #sv_validate_name, #sv_validate_parent_rank, #synonyms, #taxon_name_classifications_for_statuses, #unavailable_or_invalid?, used_recently, used_recently_in_classifications, used_recently_in_relationships, #validate_one_root_per_project, #validate_parent_from_the_same_project, #validate_parent_is_set, #validate_rank_class_class, #validate_source_type, with_taxon_name_relationship, #year_integer
Methods included from TaxonName::MatrixHooks
#coordinate_observation_matrix_row_items, #in_scope_observation_matrix_row_items, #out_of_scope_observation_matrix_row_items
Methods included from Shared::MatrixHooks::Dynamic
#dynamic_add_to_matrix_column_items, #dynamic_add_to_matrix_row_items, #dynamic_cleanup_in_scope_column_items, #dynamic_cleanup_in_scope_row_items, #dynamic_cleanup_out_of_scope_column_items, #dynamic_cleanup_out_of_scope_row_items, #dynamic_column_items_in, #dynamic_column_items_out, #dynamic_inspect_matrices, #dynamic_remove_from_matrix_column_items, #dynamic_remove_from_matrix_row_items, #dynamic_row_items_in, #dynamic_row_items_out, #dynamic_syncronize_matrices, #dynamic_update_matrix_column_items?, #dynamic_update_matrix_row_items?, #in_scope_observation_matrix_column_items, #in_scope_observation_matrix_row_items, #out_of_scope_observation_matrix_column_items, #out_of_scope_observation_matrix_row_items, #prepare_matrix_items
Methods included from Shared::MatrixHooks::Member
#member_add_matrix_columns, #member_add_matrix_rows, #member_add_to_matrix_items, #member_of_new_matrix_column_items, #member_of_new_matrix_row_items, #member_of_old_matrix_column_items, #member_of_old_matrix_row_items, #member_remove_from_matrix_items, #member_remove_matrix_columns, #member_remove_matrix_rows, #member_syncronize_matrices, #member_update_matrix_items?
Methods included from Shared::IsData
#errors_excepting, #full_error_messages_excepting, #identical, #is_community?, #is_destroyable?, #is_editable?, #is_in_use?, #is_in_users_projects?, #metamorphosize, #similar
Methods included from SoftValidation
#clear_soft_validations, #fix_soft_validations, #soft_fixed?, #soft_valid?, #soft_validate, #soft_validated?, #soft_validations
Methods included from Shared::AlternateValues
#all_values_for, #alternate_valued?
Methods included from Shared::Confidences
Methods included from Shared::Citations
#cited?, #mark_citations_for_destruction, #nomenclature_date, #origin_citation_source_id, #reject_citations, #requires_citation?, #sources_by_topic_id
Methods included from Shared::Depictions
#has_depictions?, #image_array=, #reject_depictions, #reject_images
Methods included from Shared::Notes
#concatenated_notes_string, #reject_notes
Methods included from Shared::Identifiers
#identified?, #next_by_identifier, #previous_by_identifier, #reject_identifiers
Methods included from Shared::Tags
#reject_tags, #tag_with, #tagged?, #tagged_with?
Methods included from Shared::HasRoles
Methods included from Shared::DataAttributes
#import_attributes, #internal_attributes, #keyword_value_hash, #reject_data_attributes
Methods included from Housekeeping
#has_polymorphic_relationship?
Methods inherited from ApplicationRecord
Instance Attribute Details
#combination_verbatim_name ⇒ String
Use with caution, and sparingly! If the combination of values from Protonyms can not reflect the formulation of the combination as provided by the original author that string can be provided here. The verbatim value is not further parsed. It is only provided to clarify what the combination looked like when first published. The following recommendations are made:
1) The provided string should visually reflect as close as possible what was seen in the publication itself, including
capitalization, accented characters etc.
2) The full epithet (combination) should be provided, not just the differing component part (see 3 below).
3) Misspellings can be more acurately reflected by creating new Protonyms.
Example uses:
1) Jones 1915 publishes Aus aus. Smith 1920 uses, literally "Aus (Bus) Janes 1915".
It is clear "Janes" is "Jones", therefor "Aus (Bus) Janes 1915" is provided as combination_verbatim_name.
2) Smith 1800 publishes Aus Jonesi (i.e. Aus jonesi). The combination_combination_verbatim name is used to
provide the fact that Jonesi was capitalized.
3) "Aus brocen" is used for "Aus broken". If the curators decide not to create a new protonym, perhaps because
they feel "brocen" was a printing press error that left off the straight bit of the "k" then they should minimally
include "Aus brocen" in this field, rather than just "brocen". An alternative is to create a new Protonym "brocen".
4) 'Aus (Aus)' was originally described in 1920. "(Aus)" was used in a new combination alone as "Aus". This is the only case
in which combination may contain a single protonym.
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 |
# File 'app/models/combination.rb', line 58 class Combination < TaxonName # The ranks that can be used to build combinations. ! TODO: family group names ? APPLICABLE_RANKS = %w{family subfamily tribe subtribe genus subgenus section subsection series subseries species subspecies variety subvariety form subform}.freeze before_validation :set_parent validate :validate_absence_of_subject_relationships # TODO: make access private attr_accessor :disable_combination_relationship_check # Overwritten here from TaxonName to allow for destroy has_many :related_taxon_name_relationships, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id, inverse_of: :object_taxon_name, dependent: :destroy has_many :combination_relationships, -> { joins(:taxon_name_relationships) where("taxon_name_relationships.type LIKE 'TaxonNameRelationship::Combination::%'") }, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id has_many :combination_taxon_names, through: :combination_relationships, source: :subject_taxon_name # Create syntactic helper methods TaxonNameRelationship.descendants.each do |d| if d.respond_to?(:assignment_method) if d.name.to_s =~ /TaxonNameRelationship::SourceClassifiedAs/ relationship = "#{d.assignment_method}_relationship".to_sym has_one relationship, class_name: d.name.to_s, foreign_key: :subject_taxon_name_id has_one d.assignment_method.to_sym, through: relationship, source: :object_taxon_name end if d.name.to_s =~ /TaxonNameRelationship::Combination/ # |SourceClassifiedAs relationships = "#{d.assignment_method}_relationships".to_sym has_many relationships, -> { where('taxon_name_relationships.type LIKE ?', d.name + '%') }, class_name: 'TaxonNameRelationship', foreign_key: :subject_taxon_name_id has_many d.assignment_method.to_s.pluralize.to_sym, through: relationships, source: :object_taxon_name end end if d.respond_to?(:inverse_assignment_method) if d.name.to_s =~ /TaxonNameRelationship::SourceClassifiedAs/ relationships = "#{d.inverse_assignment_method}_relationships".to_sym has_many relationships, -> { where('taxon_name_relationships.type LIKE ?', d.name + '%') }, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id has_many d.inverse_assignment_method.to_s.pluralize.to_sym, through: relationships, source: :subject_taxon_name end if d.name.to_s =~ /TaxonNameRelationship::Combination/ # |SourceClassifiedAs relationship = "#{d.inverse_assignment_method}_relationship".to_sym has_one relationship, class_name: d.name.to_s, foreign_key: :object_taxon_name_id has_one d.inverse_assignment_method.to_sym, through: relationship, source: :subject_taxon_name end end end APPLICABLE_RANKS.each do |rank| has_one "#{rank}_taxon_name_relationship".to_sym, -> { joins(:combination_relationships) where(taxon_name_relationships: {type: "TaxonNameRelationship::Combination::#{rank.capitalize}"}) }, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id has_one rank.to_sym, -> { joins(:combination_relationships) where(taxon_name_relationships: {type: "TaxonNameRelationship::Combination::#{rank.capitalize}"}) }, through: "#{rank}_taxon_name_relationship".to_sym, source: :subject_taxon_name accepts_nested_attributes_for rank.to_sym attr_accessor "#{rank}_id".to_sym method = "#{rank}_id" define_method(method) { if self.send(rank) self.send(rank).id else nil end } define_method("#{method}=") {|value| if !value.blank? if n = Protonym.find(value) self.send("#{rank}=", n) end end } end scope :with_protonym_at_rank, -> (rank, protonym) { includes(:combination_relationships). where('taxon_name_relationships.type = ? and taxon_name_relationships.subject_taxon_name_id = ?', rank, protonym). references(:combination_relationships)} validate :is_unique validate :does_not_exist_as_original_combination, unless: Proc.new {|a| a.errors..include? 'Combination exists.' } validate :parent_is_properly_set , unless: Proc.new {|a| a.errors..include? 'Combination exists.' } validate :composition, unless: Proc.new {|a| disable_combination_relationship_check == true || a.errors..include?('Combination exists.') } validates :rank_class, absence: true soft_validate(:sv_combination_duplicates, set: :combination_duplicates, has_fix: false) soft_validate(:sv_year_of_publication_matches_source, set: :dates, has_fix: false) soft_validate(:sv_year_of_publication_not_older_than_protonyms, set: :dates, has_fix: false) soft_validate(:sv_source_not_older_than_protonyms, set: :dates, has_fix: false) # @return [Protonym Scope] # @params protonym_ids [Hash] like `{genus: 4, species: 5}` # the absence of _id in the keys in part reflects integration with Biodiversity gem # AHA from http://stackoverflow.com/questions/28568205/rails-4-arel-join-on-subquery # See also Descriptor::Gene def self.protonyms_matching_original_relationships(protonym_ids = {}) protonym_ids.compact! return Protonym.none if !protonym_ids.keys.any? s = Protonym.arel_table sr = TaxonNameRelationship.arel_table j = s.alias('j') # required for group/having purposes b = s.project(j[Arel.star]).from(j) .join(sr) .on(sr['object_taxon_name_id'].eq(j['id'])) # Build an aliased join for each set of attributes protonym_ids.each do |rank, id| sr_a = sr.alias("b_#{rank}") b = b.join(sr_a).on( sr_a['object_taxon_name_id'].eq(j['id']), sr_a['type'].eq("TaxonNameRelationship::OriginalCombination::Original#{rank.capitalize}"), sr_a['subject_taxon_name_id'].eq(id) ) end b = b.group(j['id']).having(sr['object_taxon_name_id'].count.eq(protonym_ids.count)) b = b.as('join_alias') Protonym.joins(Arel::Nodes::InnerJoin.new(b, Arel::Nodes::On.new(b['id'].eq(s['id'])))) end # @return [Protonym Scope] hmmm- a Protonym class method?! # Protonyms matching original relations, if name provided then name added as an additional check on verbatim match # @params name [String, nil] the non-htmlized version of the name, without author year def self.matching_protonyms(name = nil, **protonym_ids) q = nil if name.blank? q = protonyms_matching_original_relationships(protonym_ids) else q = protonyms_matching_original_relationships(protonym_ids).where('taxon_names.cached_original_combination = ?', name) end q end # @return [Scope] # @params keyword_args [Hash] like `{genus: 123, :species: 456}` (note no `_id` suffix) def self.find_by_protonym_ids(**keyword_args) keyword_args.compact! return Combination.none if keyword_args.empty? c = Combination.arel_table r = TaxonNameRelationship.arel_table a = c.alias("a_foo") b = c.project(a[Arel.star]).from(a) .join(r) .on(r['object_taxon_name_id'].eq(a['id'])) s = [] i = 0 keyword_args.each do |rank, id| r_a = r.alias("foo_#{i}") b = b.join(r_a).on( r_a['object_taxon_name_id'].eq(a['id']), r_a['type'].eq(TAXON_NAME_RELATIONSHIP_COMBINATION_TYPES[rank]), r_a['subject_taxon_name_id'].eq(id) ) i += 1 end b = b.group(a['id']).having(r['object_taxon_name_id'].count.eq(keyword_args.keys.count)) b = b.as("z_bar") Combination.joins(Arel::Nodes::InnerJoin.new(b, Arel::Nodes::On.new(b['id'].eq(c['id'])))) end # @return [Combination, false] # @params keyword_args [Hash] like `{genus: 123, :species: 456}` (note no `_id` suffix) # the matching Combination if it exists, otherwise false # if name is provided then cached must match (i.e. verbatim_name if provided must also match) def self.match_exists?(name = nil, **keyword_args) if name.blank? a = find_by_protonym_ids(**keyword_args).first else a = find_by_protonym_ids(**keyword_args).where(cached: name).first end a ? a : false end # @return [Boolean] # true if the finest level (typically species) currently has the same parent def is_current_placement? return false if protonyms.second_to_last.nil? protonyms.last.parent_id == protonyms.second_to_last.id end # @return [Array of TaxonName] # pre-ordered by rank # TODO: hard code sort order def protonyms return protonyms_by_association if new_record? p = combination_taxon_names.sort{|a,b| RANKS.index(a.rank_string) <=> RANKS.index(b.rank_string) } return protonyms_by_association if p.empty? return p end # @return [Hash] # like `{ genus: 1, species: 2 }` def protonym_ids_params protonyms_by_rank.inject({}) {|hsh, p| hsh.merge!( p[0].to_sym => p[1].id )} end # Overrides {TaxonName#full_name_hash} # @return [Hash] def full_name_hash gender = nil data = {} protonyms_by_rank.each do |rank, i| gender = i.gender_name if rank == 'genus' if ['genus', 'subgenus', 'species', 'subspecies'].include? (rank) data[rank] = [nil, i.name_with_misspelling(gender)] else data[rank] = [i.rank_class.abbreviation, i.name_with_misspelling(gender)] end end if data['genus'].nil? data['genus'] = [nil, "[GENUS NOT SPECIFIED]"] end if data['species'].nil? && (!data['subspecies'].nil? || !data['variety'].nil? || !data['subvariety'].nil? || !data['form'].nil? || !data['subform'].nil?) data['species'] = [nil, "[SPECIES NOT SPECIFIED]"] end if data['variety'].nil? && !data['subvariety'].nil? data['variety'] = [nil, "[VARIETY NOT SPECIFIED]"] end if data['form'].nil? && !data['subform'].nil? data['form'] = [nil, "[FORM NOT SPECIFIED]"] end data end # @return [Hash of {rank: Protonym}, nil] # the component names for this combination prior to it being saved (used to return values prior to save) def protonyms_by_rank result = {} APPLICABLE_RANKS.each do |rank| if protonym = send(rank) result[rank] = protonym end end result end # @return [Array of Integers] # the collective years the protonyms were (nomenclaturaly) published on (ordered from genus to below) def publication_years description_years = protonyms.collect{|a| a.nomenclature_date ? a.nomenclature_date.year : nil}.compact end # @return [Integer, nil] # the earliest year (nomenclature) that a component Protonym was published on def earliest_protonym_year publication_years.sort.first end # return [Array of TaxonNameRelationship] # classes that are applicable to this name, as deterimned by Rank def combination_class_relationships(rank_string) relations = [] TaxonNameRelationship::Combination.descendants.each do |r| relations.push(r) if r.valid_object_ranks.include?(rank_string) end relations end # TODO: DEPRECATE this is likely not required in our new interfaces def combination_relationships_and_stubs(rank_string) display_order = [ :combination_genus, :combination_subgenus, :combination_species, :combination_subspecies, :combination_variety, :combination_form ] defined_relations = combination_relationships.all created_already = defined_relations.collect{|a| a.class} new_relations = [] combination_class_relationships(rank_string).each do |r| new_relations.push( r.new(object_taxon_name: self) ) if !created_already.include?(r) end (new_relations + defined_relations).sort{|a,b| display_order.index(a.class.inverse_assignment_method) <=> display_order.index(b.class.inverse_assignment_method) } end def get_valid_taxon_name c = protonyms_by_rank return self if c.empty? c[c.keys.last].valid_taxon_name end def finest_protonym protonyms_by_rank.values.last end def ay = ay.blank? ? nil : ay end # @return [Array of TaxonNames, nil] # return the component names for this combination prior to it being saved def protonyms_by_association APPLICABLE_RANKS.collect{|r| self.send(r)}.compact end protected def validate_absence_of_subject_relationships if TaxonNameRelationship.where(subject_taxon_name_id: self.id).any? errors.add(:base, 'This combination could not be used as a Subject in any TaxonNameRelationships.') end end # TODO: this is a TaxonName level validation, it doesn't belong here def sv_year_of_publication_matches_source source_year = source.nomenclature_year if source if year_of_publication && source_year soft_validations.add(:year_of_publication, 'The published date of the combination is not the same as provided by the original publication') if source_year != year_of_publication end end def sv_source_not_older_than_protonyms source_year = source.try(:nomenclature_year) target_year = earliest_protonym_year if source_year && target_year soft_validations.add(:base, "The publication date of combination (#{source_year}) is older than the original publication date of one of the name in the combination (#{target_year}") if source_year < target_year end end def sv_year_of_publication_not_older_than_protonyms if year_of_publication && earliest_protonym_year soft_validations.add(:year_of_publication, "The publication date of combination (#{year_of_publication}) is older than the original publication date of one of the name in the combination (#{earliest_protonym_year}") if year_of_publication < earliest_protonym_year end end def sv_combination_duplicates duplicate = Combination.not_self(self).where(cached: cached) soft_validations.add(:base, 'Combination is a duplicate') unless duplicate.empty? end def set_parent names = protonyms write_attribute(:parent_id, names.first.parent.id) if names.count > 0 && names.first.parent end # The parent of a Combination is the parent of the highest ranked protonym in that Combination def parent_is_properly_set check = protonyms.first if parent && check && check.parent errors.add(:base, 'Parent is not highest ranked name.') if parent != check.parent end end def composition c = protonyms.count if c == 0 errors.add(:base, 'Combination includes no names.') return end protonyms.each do |p| if !p.is_genus_or_species_rank? errors.add(:base, 'Combination includes one or more non-species or genus group names.') return end end # There are more than one protonyms, which seem to be valid elements p = protonyms.last errors.add(:base, 'Combination includes only one name and that is name is not a genus name.') if c < 2 && p.is_species_rank? errors.add(:base, 'Combination includes more than two genus group names.') if c > 2 && p.is_genus_rank? end def is_unique if a = Combination.match_exists?(verbatim_name, **protonym_ids_params) errors.add(:base, 'Combination exists.') if a.id != id end end def does_not_exist_as_original_combination if a = Combination.matching_protonyms(get_full_name, **protonym_ids_params) errors.add(:base, "Combination exists as protonym(s) with matching original combination: #{a.all.pluck(:cached).join(', ')}.") if a.any? end end end |
#disable_combination_relationship_check ⇒ Object
TODO: make access private
68 69 70 |
# File 'app/models/combination.rb', line 68 def disable_combination_relationship_check @disable_combination_relationship_check end |
#parent_id ⇒ Integer
the parent is the parent of the highest ranked component Protonym, it is automatically set i.e. it should never be assigned directly
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 |
# File 'app/models/combination.rb', line 58 class Combination < TaxonName # The ranks that can be used to build combinations. ! TODO: family group names ? APPLICABLE_RANKS = %w{family subfamily tribe subtribe genus subgenus section subsection series subseries species subspecies variety subvariety form subform}.freeze before_validation :set_parent validate :validate_absence_of_subject_relationships # TODO: make access private attr_accessor :disable_combination_relationship_check # Overwritten here from TaxonName to allow for destroy has_many :related_taxon_name_relationships, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id, inverse_of: :object_taxon_name, dependent: :destroy has_many :combination_relationships, -> { joins(:taxon_name_relationships) where("taxon_name_relationships.type LIKE 'TaxonNameRelationship::Combination::%'") }, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id has_many :combination_taxon_names, through: :combination_relationships, source: :subject_taxon_name # Create syntactic helper methods TaxonNameRelationship.descendants.each do |d| if d.respond_to?(:assignment_method) if d.name.to_s =~ /TaxonNameRelationship::SourceClassifiedAs/ relationship = "#{d.assignment_method}_relationship".to_sym has_one relationship, class_name: d.name.to_s, foreign_key: :subject_taxon_name_id has_one d.assignment_method.to_sym, through: relationship, source: :object_taxon_name end if d.name.to_s =~ /TaxonNameRelationship::Combination/ # |SourceClassifiedAs relationships = "#{d.assignment_method}_relationships".to_sym has_many relationships, -> { where('taxon_name_relationships.type LIKE ?', d.name + '%') }, class_name: 'TaxonNameRelationship', foreign_key: :subject_taxon_name_id has_many d.assignment_method.to_s.pluralize.to_sym, through: relationships, source: :object_taxon_name end end if d.respond_to?(:inverse_assignment_method) if d.name.to_s =~ /TaxonNameRelationship::SourceClassifiedAs/ relationships = "#{d.inverse_assignment_method}_relationships".to_sym has_many relationships, -> { where('taxon_name_relationships.type LIKE ?', d.name + '%') }, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id has_many d.inverse_assignment_method.to_s.pluralize.to_sym, through: relationships, source: :subject_taxon_name end if d.name.to_s =~ /TaxonNameRelationship::Combination/ # |SourceClassifiedAs relationship = "#{d.inverse_assignment_method}_relationship".to_sym has_one relationship, class_name: d.name.to_s, foreign_key: :object_taxon_name_id has_one d.inverse_assignment_method.to_sym, through: relationship, source: :subject_taxon_name end end end APPLICABLE_RANKS.each do |rank| has_one "#{rank}_taxon_name_relationship".to_sym, -> { joins(:combination_relationships) where(taxon_name_relationships: {type: "TaxonNameRelationship::Combination::#{rank.capitalize}"}) }, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id has_one rank.to_sym, -> { joins(:combination_relationships) where(taxon_name_relationships: {type: "TaxonNameRelationship::Combination::#{rank.capitalize}"}) }, through: "#{rank}_taxon_name_relationship".to_sym, source: :subject_taxon_name accepts_nested_attributes_for rank.to_sym attr_accessor "#{rank}_id".to_sym method = "#{rank}_id" define_method(method) { if self.send(rank) self.send(rank).id else nil end } define_method("#{method}=") {|value| if !value.blank? if n = Protonym.find(value) self.send("#{rank}=", n) end end } end scope :with_protonym_at_rank, -> (rank, protonym) { includes(:combination_relationships). where('taxon_name_relationships.type = ? and taxon_name_relationships.subject_taxon_name_id = ?', rank, protonym). references(:combination_relationships)} validate :is_unique validate :does_not_exist_as_original_combination, unless: Proc.new {|a| a.errors..include? 'Combination exists.' } validate :parent_is_properly_set , unless: Proc.new {|a| a.errors..include? 'Combination exists.' } validate :composition, unless: Proc.new {|a| disable_combination_relationship_check == true || a.errors..include?('Combination exists.') } validates :rank_class, absence: true soft_validate(:sv_combination_duplicates, set: :combination_duplicates, has_fix: false) soft_validate(:sv_year_of_publication_matches_source, set: :dates, has_fix: false) soft_validate(:sv_year_of_publication_not_older_than_protonyms, set: :dates, has_fix: false) soft_validate(:sv_source_not_older_than_protonyms, set: :dates, has_fix: false) # @return [Protonym Scope] # @params protonym_ids [Hash] like `{genus: 4, species: 5}` # the absence of _id in the keys in part reflects integration with Biodiversity gem # AHA from http://stackoverflow.com/questions/28568205/rails-4-arel-join-on-subquery # See also Descriptor::Gene def self.protonyms_matching_original_relationships(protonym_ids = {}) protonym_ids.compact! return Protonym.none if !protonym_ids.keys.any? s = Protonym.arel_table sr = TaxonNameRelationship.arel_table j = s.alias('j') # required for group/having purposes b = s.project(j[Arel.star]).from(j) .join(sr) .on(sr['object_taxon_name_id'].eq(j['id'])) # Build an aliased join for each set of attributes protonym_ids.each do |rank, id| sr_a = sr.alias("b_#{rank}") b = b.join(sr_a).on( sr_a['object_taxon_name_id'].eq(j['id']), sr_a['type'].eq("TaxonNameRelationship::OriginalCombination::Original#{rank.capitalize}"), sr_a['subject_taxon_name_id'].eq(id) ) end b = b.group(j['id']).having(sr['object_taxon_name_id'].count.eq(protonym_ids.count)) b = b.as('join_alias') Protonym.joins(Arel::Nodes::InnerJoin.new(b, Arel::Nodes::On.new(b['id'].eq(s['id'])))) end # @return [Protonym Scope] hmmm- a Protonym class method?! # Protonyms matching original relations, if name provided then name added as an additional check on verbatim match # @params name [String, nil] the non-htmlized version of the name, without author year def self.matching_protonyms(name = nil, **protonym_ids) q = nil if name.blank? q = protonyms_matching_original_relationships(protonym_ids) else q = protonyms_matching_original_relationships(protonym_ids).where('taxon_names.cached_original_combination = ?', name) end q end # @return [Scope] # @params keyword_args [Hash] like `{genus: 123, :species: 456}` (note no `_id` suffix) def self.find_by_protonym_ids(**keyword_args) keyword_args.compact! return Combination.none if keyword_args.empty? c = Combination.arel_table r = TaxonNameRelationship.arel_table a = c.alias("a_foo") b = c.project(a[Arel.star]).from(a) .join(r) .on(r['object_taxon_name_id'].eq(a['id'])) s = [] i = 0 keyword_args.each do |rank, id| r_a = r.alias("foo_#{i}") b = b.join(r_a).on( r_a['object_taxon_name_id'].eq(a['id']), r_a['type'].eq(TAXON_NAME_RELATIONSHIP_COMBINATION_TYPES[rank]), r_a['subject_taxon_name_id'].eq(id) ) i += 1 end b = b.group(a['id']).having(r['object_taxon_name_id'].count.eq(keyword_args.keys.count)) b = b.as("z_bar") Combination.joins(Arel::Nodes::InnerJoin.new(b, Arel::Nodes::On.new(b['id'].eq(c['id'])))) end # @return [Combination, false] # @params keyword_args [Hash] like `{genus: 123, :species: 456}` (note no `_id` suffix) # the matching Combination if it exists, otherwise false # if name is provided then cached must match (i.e. verbatim_name if provided must also match) def self.match_exists?(name = nil, **keyword_args) if name.blank? a = find_by_protonym_ids(**keyword_args).first else a = find_by_protonym_ids(**keyword_args).where(cached: name).first end a ? a : false end # @return [Boolean] # true if the finest level (typically species) currently has the same parent def is_current_placement? return false if protonyms.second_to_last.nil? protonyms.last.parent_id == protonyms.second_to_last.id end # @return [Array of TaxonName] # pre-ordered by rank # TODO: hard code sort order def protonyms return protonyms_by_association if new_record? p = combination_taxon_names.sort{|a,b| RANKS.index(a.rank_string) <=> RANKS.index(b.rank_string) } return protonyms_by_association if p.empty? return p end # @return [Hash] # like `{ genus: 1, species: 2 }` def protonym_ids_params protonyms_by_rank.inject({}) {|hsh, p| hsh.merge!( p[0].to_sym => p[1].id )} end # Overrides {TaxonName#full_name_hash} # @return [Hash] def full_name_hash gender = nil data = {} protonyms_by_rank.each do |rank, i| gender = i.gender_name if rank == 'genus' if ['genus', 'subgenus', 'species', 'subspecies'].include? (rank) data[rank] = [nil, i.name_with_misspelling(gender)] else data[rank] = [i.rank_class.abbreviation, i.name_with_misspelling(gender)] end end if data['genus'].nil? data['genus'] = [nil, "[GENUS NOT SPECIFIED]"] end if data['species'].nil? && (!data['subspecies'].nil? || !data['variety'].nil? || !data['subvariety'].nil? || !data['form'].nil? || !data['subform'].nil?) data['species'] = [nil, "[SPECIES NOT SPECIFIED]"] end if data['variety'].nil? && !data['subvariety'].nil? data['variety'] = [nil, "[VARIETY NOT SPECIFIED]"] end if data['form'].nil? && !data['subform'].nil? data['form'] = [nil, "[FORM NOT SPECIFIED]"] end data end # @return [Hash of {rank: Protonym}, nil] # the component names for this combination prior to it being saved (used to return values prior to save) def protonyms_by_rank result = {} APPLICABLE_RANKS.each do |rank| if protonym = send(rank) result[rank] = protonym end end result end # @return [Array of Integers] # the collective years the protonyms were (nomenclaturaly) published on (ordered from genus to below) def publication_years description_years = protonyms.collect{|a| a.nomenclature_date ? a.nomenclature_date.year : nil}.compact end # @return [Integer, nil] # the earliest year (nomenclature) that a component Protonym was published on def earliest_protonym_year publication_years.sort.first end # return [Array of TaxonNameRelationship] # classes that are applicable to this name, as deterimned by Rank def combination_class_relationships(rank_string) relations = [] TaxonNameRelationship::Combination.descendants.each do |r| relations.push(r) if r.valid_object_ranks.include?(rank_string) end relations end # TODO: DEPRECATE this is likely not required in our new interfaces def combination_relationships_and_stubs(rank_string) display_order = [ :combination_genus, :combination_subgenus, :combination_species, :combination_subspecies, :combination_variety, :combination_form ] defined_relations = combination_relationships.all created_already = defined_relations.collect{|a| a.class} new_relations = [] combination_class_relationships(rank_string).each do |r| new_relations.push( r.new(object_taxon_name: self) ) if !created_already.include?(r) end (new_relations + defined_relations).sort{|a,b| display_order.index(a.class.inverse_assignment_method) <=> display_order.index(b.class.inverse_assignment_method) } end def get_valid_taxon_name c = protonyms_by_rank return self if c.empty? c[c.keys.last].valid_taxon_name end def finest_protonym protonyms_by_rank.values.last end def ay = ay.blank? ? nil : ay end # @return [Array of TaxonNames, nil] # return the component names for this combination prior to it being saved def protonyms_by_association APPLICABLE_RANKS.collect{|r| self.send(r)}.compact end protected def validate_absence_of_subject_relationships if TaxonNameRelationship.where(subject_taxon_name_id: self.id).any? errors.add(:base, 'This combination could not be used as a Subject in any TaxonNameRelationships.') end end # TODO: this is a TaxonName level validation, it doesn't belong here def sv_year_of_publication_matches_source source_year = source.nomenclature_year if source if year_of_publication && source_year soft_validations.add(:year_of_publication, 'The published date of the combination is not the same as provided by the original publication') if source_year != year_of_publication end end def sv_source_not_older_than_protonyms source_year = source.try(:nomenclature_year) target_year = earliest_protonym_year if source_year && target_year soft_validations.add(:base, "The publication date of combination (#{source_year}) is older than the original publication date of one of the name in the combination (#{target_year}") if source_year < target_year end end def sv_year_of_publication_not_older_than_protonyms if year_of_publication && earliest_protonym_year soft_validations.add(:year_of_publication, "The publication date of combination (#{year_of_publication}) is older than the original publication date of one of the name in the combination (#{earliest_protonym_year}") if year_of_publication < earliest_protonym_year end end def sv_combination_duplicates duplicate = Combination.not_self(self).where(cached: cached) soft_validations.add(:base, 'Combination is a duplicate') unless duplicate.empty? end def set_parent names = protonyms write_attribute(:parent_id, names.first.parent.id) if names.count > 0 && names.first.parent end # The parent of a Combination is the parent of the highest ranked protonym in that Combination def parent_is_properly_set check = protonyms.first if parent && check && check.parent errors.add(:base, 'Parent is not highest ranked name.') if parent != check.parent end end def composition c = protonyms.count if c == 0 errors.add(:base, 'Combination includes no names.') return end protonyms.each do |p| if !p.is_genus_or_species_rank? errors.add(:base, 'Combination includes one or more non-species or genus group names.') return end end # There are more than one protonyms, which seem to be valid elements p = protonyms.last errors.add(:base, 'Combination includes only one name and that is name is not a genus name.') if c < 2 && p.is_species_rank? errors.add(:base, 'Combination includes more than two genus group names.') if c > 2 && p.is_genus_rank? end def is_unique if a = Combination.match_exists?(verbatim_name, **protonym_ids_params) errors.add(:base, 'Combination exists.') if a.id != id end end def does_not_exist_as_original_combination if a = Combination.matching_protonyms(get_full_name, **protonym_ids_params) errors.add(:base, "Combination exists as protonym(s) with matching original combination: #{a.all.pluck(:cached).join(', ')}.") if a.any? end end end |
Class Method Details
.find_by_protonym_ids(**keyword_args) ⇒ Scope
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'app/models/combination.rb', line 218 def self.find_by_protonym_ids(**keyword_args) keyword_args.compact! return Combination.none if keyword_args.empty? c = Combination.arel_table r = TaxonNameRelationship.arel_table a = c.alias("a_foo") b = c.project(a[Arel.star]).from(a) .join(r) .on(r['object_taxon_name_id'].eq(a['id'])) s = [] i = 0 keyword_args.each do |rank, id| r_a = r.alias("foo_#{i}") b = b.join(r_a).on( r_a['object_taxon_name_id'].eq(a['id']), r_a['type'].eq(TAXON_NAME_RELATIONSHIP_COMBINATION_TYPES[rank]), r_a['subject_taxon_name_id'].eq(id) ) i += 1 end b = b.group(a['id']).having(r['object_taxon_name_id'].count.eq(keyword_args.keys.count)) b = b.as("z_bar") Combination.joins(Arel::Nodes::InnerJoin.new(b, Arel::Nodes::On.new(b['id'].eq(c['id'])))) end |
.match_exists?(name = nil, **keyword_args) ⇒ Combination, false
254 255 256 257 258 259 260 261 |
# File 'app/models/combination.rb', line 254 def self.match_exists?(name = nil, **keyword_args) if name.blank? a = find_by_protonym_ids(**keyword_args).first else a = find_by_protonym_ids(**keyword_args).where(cached: name).first end a ? a : false end |
.matching_protonyms(name = nil, **protonym_ids) ⇒ Protonym Scope
Returns hmmm- a Protonym class method?! Protonyms matching original relations, if name provided then name added as an additional check on verbatim match.
206 207 208 209 210 211 212 213 214 |
# File 'app/models/combination.rb', line 206 def self.matching_protonyms(name = nil, **protonym_ids) q = nil if name.blank? q = protonyms_matching_original_relationships(protonym_ids) else q = protonyms_matching_original_relationships(protonym_ids).where('taxon_names.cached_original_combination = ?', name) end q end |
.protonyms_matching_original_relationships(protonym_ids = {}) ⇒ Object
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'app/models/combination.rb', line 173 def self.protonyms_matching_original_relationships(protonym_ids = {}) protonym_ids.compact! return Protonym.none if !protonym_ids.keys.any? s = Protonym.arel_table sr = TaxonNameRelationship.arel_table j = s.alias('j') # required for group/having purposes b = s.project(j[Arel.star]).from(j) .join(sr) .on(sr['object_taxon_name_id'].eq(j['id'])) # Build an aliased join for each set of attributes protonym_ids.each do |rank, id| sr_a = sr.alias("b_#{rank}") b = b.join(sr_a).on( sr_a['object_taxon_name_id'].eq(j['id']), sr_a['type'].eq("TaxonNameRelationship::OriginalCombination::Original#{rank.capitalize}"), sr_a['subject_taxon_name_id'].eq(id) ) end b = b.group(j['id']).having(sr['object_taxon_name_id'].count.eq(protonym_ids.count)) b = b.as('join_alias') Protonym.joins(Arel::Nodes::InnerJoin.new(b, Arel::Nodes::On.new(b['id'].eq(s['id'])))) end |
Instance Method Details
#combination_class_relationships(rank_string) ⇒ Object
return [Array of TaxonNameRelationship]
classes that are applicable to this name, as deterimned by Rank
341 342 343 344 345 346 347 |
# File 'app/models/combination.rb', line 341 def combination_class_relationships(rank_string) relations = [] TaxonNameRelationship::Combination.descendants.each do |r| relations.push(r) if r.valid_object_ranks.include?(rank_string) end relations end |
#combination_relationships_and_stubs(rank_string) ⇒ Object
TODO: DEPRECATE this is likely not required in our new interfaces
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 |
# File 'app/models/combination.rb', line 350 def combination_relationships_and_stubs(rank_string) display_order = [ :combination_genus, :combination_subgenus, :combination_species, :combination_subspecies, :combination_variety, :combination_form ] defined_relations = combination_relationships.all created_already = defined_relations.collect{|a| a.class} new_relations = [] combination_class_relationships(rank_string).each do |r| new_relations.push( r.new(object_taxon_name: self) ) if !created_already.include?(r) end (new_relations + defined_relations).sort{|a,b| display_order.index(a.class.inverse_assignment_method) <=> display_order.index(b.class.inverse_assignment_method) } end |
#composition ⇒ Object (protected)
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 |
# File 'app/models/combination.rb', line 437 def composition c = protonyms.count if c == 0 errors.add(:base, 'Combination includes no names.') return end protonyms.each do |p| if !p.is_genus_or_species_rank? errors.add(:base, 'Combination includes one or more non-species or genus group names.') return end end # There are more than one protonyms, which seem to be valid elements p = protonyms.last errors.add(:base, 'Combination includes only one name and that is name is not a genus name.') if c < 2 && p.is_species_rank? errors.add(:base, 'Combination includes more than two genus group names.') if c > 2 && p.is_genus_rank? end |
#does_not_exist_as_original_combination ⇒ Object (protected)
464 465 466 467 468 |
# File 'app/models/combination.rb', line 464 def does_not_exist_as_original_combination if a = Combination.matching_protonyms(get_full_name, **protonym_ids_params) errors.add(:base, "Combination exists as protonym(s) with matching original combination: #{a.all.pluck(:cached).join(', ')}.") if a.any? end end |
#earliest_protonym_year ⇒ Integer?
Returns the earliest year (nomenclature) that a component Protonym was published on.
335 336 337 |
# File 'app/models/combination.rb', line 335 def earliest_protonym_year publication_years.sort.first end |
#finest_protonym ⇒ Object
374 375 376 |
# File 'app/models/combination.rb', line 374 def finest_protonym protonyms_by_rank.values.last end |
#full_name_hash ⇒ Hash
Overrides TaxonName#full_name_hash
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'app/models/combination.rb', line 288 def full_name_hash gender = nil data = {} protonyms_by_rank.each do |rank, i| gender = i.gender_name if rank == 'genus' if ['genus', 'subgenus', 'species', 'subspecies'].include? (rank) data[rank] = [nil, i.name_with_misspelling(gender)] else data[rank] = [i.rank_class.abbreviation, i.name_with_misspelling(gender)] end end if data['genus'].nil? data['genus'] = [nil, "[GENUS NOT SPECIFIED]"] end if data['species'].nil? && (!data['subspecies'].nil? || !data['variety'].nil? || !data['subvariety'].nil? || !data['form'].nil? || !data['subform'].nil?) data['species'] = [nil, "[SPECIES NOT SPECIFIED]"] end if data['variety'].nil? && !data['subvariety'].nil? data['variety'] = [nil, "[VARIETY NOT SPECIFIED]"] end if data['form'].nil? && !data['subform'].nil? data['form'] = [nil, "[FORM NOT SPECIFIED]"] end data end |
#get_author_and_year ⇒ Object
378 379 380 381 |
# File 'app/models/combination.rb', line 378 def ay = ay.blank? ? nil : ay end |
#get_valid_taxon_name ⇒ Object
368 369 370 371 372 |
# File 'app/models/combination.rb', line 368 def get_valid_taxon_name c = protonyms_by_rank return self if c.empty? c[c.keys.last].valid_taxon_name end |
#is_current_placement? ⇒ Boolean
Returns true if the finest level (typically species) currently has the same parent.
265 266 267 268 |
# File 'app/models/combination.rb', line 265 def is_current_placement? return false if protonyms.second_to_last.nil? protonyms.last.parent_id == protonyms.second_to_last.id end |
#is_unique ⇒ Object (protected)
458 459 460 461 462 |
# File 'app/models/combination.rb', line 458 def is_unique if a = Combination.match_exists?(verbatim_name, **protonym_ids_params) errors.add(:base, 'Combination exists.') if a.id != id end end |
#parent_is_properly_set ⇒ Object (protected)
The parent of a Combination is the parent of the highest ranked protonym in that Combination
430 431 432 433 434 435 |
# File 'app/models/combination.rb', line 430 def parent_is_properly_set check = protonyms.first if parent && check && check.parent errors.add(:base, 'Parent is not highest ranked name.') if parent != check.parent end end |
#protonym_ids_params ⇒ Hash
Returns like `{ genus: 1, species: 2 }`.
282 283 284 |
# File 'app/models/combination.rb', line 282 def protonym_ids_params protonyms_by_rank.inject({}) {|hsh, p| hsh.merge!( p[0].to_sym => p[1].id )} end |
#protonyms ⇒ Array of TaxonName
TODO: hard code sort order
273 274 275 276 277 278 |
# File 'app/models/combination.rb', line 273 def protonyms return protonyms_by_association if new_record? p = combination_taxon_names.sort{|a,b| RANKS.index(a.rank_string) <=> RANKS.index(b.rank_string) } return protonyms_by_association if p.empty? return p end |
#protonyms_by_association ⇒ Array of TaxonNames?
Return the component names for this combination prior to it being saved
385 386 387 |
# File 'app/models/combination.rb', line 385 def protonyms_by_association APPLICABLE_RANKS.collect{|r| self.send(r)}.compact end |
#protonyms_by_rank ⇒ Hash of {rank: Protonym}?
Returns the component names for this combination prior to it being saved (used to return values prior to save).
317 318 319 320 321 322 323 324 325 |
# File 'app/models/combination.rb', line 317 def protonyms_by_rank result = {} APPLICABLE_RANKS.each do |rank| if protonym = send(rank) result[rank] = protonym end end result end |
#publication_years ⇒ Array of Integers
Returns the collective years the protonyms were (nomenclaturaly) published on (ordered from genus to below).
329 330 331 |
# File 'app/models/combination.rb', line 329 def publication_years description_years = protonyms.collect{|a| a.nomenclature_date ? a.nomenclature_date.year : nil}.compact end |
#set_parent ⇒ Object (protected)
424 425 426 427 |
# File 'app/models/combination.rb', line 424 def set_parent names = protonyms write_attribute(:parent_id, names.first.parent.id) if names.count > 0 && names.first.parent end |
#sv_combination_duplicates ⇒ Object (protected)
419 420 421 422 |
# File 'app/models/combination.rb', line 419 def sv_combination_duplicates duplicate = Combination.not_self(self).where(cached: cached) soft_validations.add(:base, 'Combination is a duplicate') unless duplicate.empty? end |
#sv_source_not_older_than_protonyms ⇒ Object (protected)
405 406 407 408 409 410 411 |
# File 'app/models/combination.rb', line 405 def sv_source_not_older_than_protonyms source_year = source.try(:nomenclature_year) target_year = earliest_protonym_year if source_year && target_year soft_validations.add(:base, "The publication date of combination (#{source_year}) is older than the original publication date of one of the name in the combination (#{target_year}") if source_year < target_year end end |
#sv_year_of_publication_matches_source ⇒ Object (protected)
TODO: this is a TaxonName level validation, it doesn't belong here
398 399 400 401 402 403 |
# File 'app/models/combination.rb', line 398 def sv_year_of_publication_matches_source source_year = source.nomenclature_year if source if year_of_publication && source_year soft_validations.add(:year_of_publication, 'The published date of the combination is not the same as provided by the original publication') if source_year != year_of_publication end end |
#sv_year_of_publication_not_older_than_protonyms ⇒ Object (protected)
413 414 415 416 417 |
# File 'app/models/combination.rb', line 413 def sv_year_of_publication_not_older_than_protonyms if year_of_publication && earliest_protonym_year soft_validations.add(:year_of_publication, "The publication date of combination (#{year_of_publication}) is older than the original publication date of one of the name in the combination (#{earliest_protonym_year}") if year_of_publication < earliest_protonym_year end end |
#validate_absence_of_subject_relationships ⇒ Object (protected)
391 392 393 394 395 |
# File 'app/models/combination.rb', line 391 def validate_absence_of_subject_relationships if TaxonNameRelationship.where(subject_taxon_name_id: self.id).any? errors.add(:base, 'This combination could not be used as a Subject in any TaxonNameRelationships.') end end |