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 a Combination pointing to the corresponding TaxonNameRelationship type
%w{genus subgenus section subsection series subseries species subspecies variety subvariety form subform}.inject({}){|hsh,r| hsh[r] = "TaxonNameRelationship::Combination::#{r.capitalize}"; hsh; }.freeze
Constants inherited from TaxonName
TaxonName::ALTERNATE_VALUES_FOR, TaxonName::COMBINATION_ELEMENTS, TaxonName::NOMEN_VALID, TaxonName::NOT_LATIN, TaxonName::NO_CACHED_MESSAGE, TaxonName::ROOT_NAME, 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.
-
#protonyms_by_rank ⇒ Hash of {rank: Protonym}?
The component names for this combination, sorted in order _prior to it being saved_.
Attributes inherited from TaxonName
#also_create_otu, #cached, #cached_author, #cached_author_year, #cached_classified_as, #cached_html, #cached_misspelling, #cached_original_combination, #cached_original_combination_html, #cached_primary_homonym, #cached_primary_homonym_alternative_spelling, #cached_secondary_homonym, #cached_secondary_homonym_alternative_spelling, #etymology, #feminine_name, #masculine_name, #name, #neuter_name, #no_cached, #project_id, #rank_class, #type, #verbatim_author, #verbatim_name, #year_of_publication
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 = {}) ⇒ Protonym Scope
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.
-
#combination_taxonomy ⇒ Object
TODO: add higher classifcation here.
- #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
Is this used before persistence of the complete Combination?! Overrides TaxonName#full_name_hash.
- #get_full_name ⇒ 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
Pre-ordered by rank.
-
#protonyms_by_association ⇒ Array of TaxonNames?
Return the component names for this combination prior to it being saved.
-
#publication_years ⇒ Array of Integers
The collective years the protonyms were (nomenclaturaly) published on (ordered from genus to below).
- #reset_protonyms_by_rank ⇒ Object protected
- #set_parent ⇒ Object protected
- #sv_author_and_year_is_not_required ⇒ Object protected
- #sv_cached_names ⇒ Object protected
- #sv_combination_duplicates ⇒ Object protected
- #sv_combination_linked_to_valid_name ⇒ Object protected
- #sv_fix_author_and_year_is_not_required ⇒ Object protected
- #sv_fix_combination_parent_update ⇒ Object protected
- #sv_fix_redundant_verbatim_name ⇒ Object protected
- #sv_redundant_verbatim_name ⇒ Object protected
- #sv_source_not_older_than_protonyms ⇒ Object protected
- #sv_year_of_publication_matches_source ⇒ Object protected
- #sv_year_of_publication_not_older_than_protonyms ⇒ Object protected
- #taxonomy(rebuild = false) ⇒ Object
- #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_html_name_and_author_year, #cached_html_original_name_and_author_year, #cached_name_and_author_year, calculated_invalid, calculated_valid, #check_for_children, #check_new_parent_class, #check_new_rank_class, #classification_invalid_or_unavailable?, #classification_unavailable?, #classification_valid?, #clear_cached, #combination_list_all, #combination_list_self, #combined_statuses, #create_otu, #descendant_protonyms, #dwc_occurrences, #first_possible_invalid_taxan_name_relationship, #first_possible_valid_taxon_name, #first_possible_valid_taxon_name_relationship, foo, #full_name, #full_name_array, #gbif_status_array, #gender_class, #gender_instance, #gender_name, #genderized_name, #get_author, #get_author_and_year, #get_cached_classified_as, #get_cached_misspelling, #get_full_name_html, #get_genus_species, #get_original_combination, #get_original_combination_html, #icn_author_and_year, #iczn_author_and_year, #is_ambiguously_invalid?, #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, #merge_to, #minimum_invalidating_year, #minimum_years_valid, #name_in_gender, #name_is_misapplied?, #name_with_misspelling, #next_sibling, #nomeclatural_history, #nomenclature_date, #normalized_genus, #not_binominal?, not_leaves, #original_author_year, out_of_scope_combinations, #out_of_scope_combinations, #part_of_speech_class, #part_of_speech_instance, #part_of_speech_name, #previous_sibling, #rank, #rank_string, #reified_id, #related_taxon_names, #relationship_invalid?, #safe_self_and_ancestors, select_optimized, #set_cached, #set_cached_author, #set_cached_author_columns, #set_cached_author_year, #set_cached_classified_as, #set_cached_is_valid, #set_cached_nomenclature_date, #set_cached_valid_taxon_name_id, #set_cached_warnings, sort_by_rank, #statuses_from_classifications, #statuses_from_relationships, #sv_conflicting_subordinate_taxa, #sv_fix_cached_names, #sv_fix_parent_is_valid_name, #sv_homotypic_synonyms, #sv_hybrid_name_relationships, #sv_incomplete_combination, #sv_missing_classifications, #sv_missing_confidence_level, #sv_missing_etymology, #sv_missing_original_publication, #sv_missing_relationships, #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_classification_minimum_invalidating_year, #taxon_name_classifications_for_statuses, #taxon_name_relationship_minimum_invalidating_year, #unavailable_or_invalid?, used_recently, used_recently_in_classifications, used_recently_in_relationships, #validate_parent_from_the_same_project, #validate_rank_class_class, #validate_root_name_is_root, #validate_source_type, with_taxon_name_relationship, #year_integer
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 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::QueryBatchUpdate
Methods included from SoftValidation
#clear_soft_validations, #fix_for, #fix_soft_validations, #soft_fixed?, #soft_valid?, #soft_validate, #soft_validated?, #soft_validations, #soft_validators
Methods included from Shared::Labels
Methods included from Shared::HasPapertrail
#attribute_updated, #attribute_updater
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
#dwc_occurrence_id, #identified?, #next_by_identifier, #previous_by_identifier, #reject_identifiers, #uri, #uuid
Methods included from Shared::Tags
#reject_tags, #tag_with, #tagged?, #tagged_with?
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 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 |
# File 'app/models/combination.rb', line 58 class Combination < TaxonName # The ranks that can be used to build a Combination pointing to the corresponding TaxonNameRelationship type APPLICABLE_RANKS = %w{genus subgenus section subsection series subseries species subspecies variety subvariety form subform}.inject({}){|hsh,r| hsh[r] = "TaxonNameRelationship::Combination::#{r.capitalize}"; hsh; }.freeze before_validation :set_parent # Before we set cached ensure we draw current data after_save :reset_protonyms_by_rank validate :validate_absence_of_subject_relationships # TODO: make access private attr_accessor :disable_combination_relationship_check # Memoize. attr_accessor :protonyms_by_rank # 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.keys.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, inverse_of: :object_taxon_name 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 accepts_nested_attributes_for "#{rank}_taxon_name_relationship".to_sym, allow_destroy: true 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.present? 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_redundant_verbatim_name, set: :cached, fix: :sv_fix_redundant_verbatim_name, name: 'Redundant verbatim name', description: 'Verbatim name is present but identical to computed name') soft_validate( :sv_combination_duplicates, set: :combination_duplicates, name: 'Duplicate combination', description: 'Combination is a duplicate' ) soft_validate( :sv_year_of_publication_matches_source, set: :dates, name: 'Year of publication does not match the source', description: 'The published date of the combination is not the same as provided by the original publication' ) soft_validate( :sv_year_of_publication_not_older_than_protonyms, set: :dates, name: 'Varbatim year in combination older than in protonyms', description: 'The varbatim year in combination is older than in protonyms in the combination' ) soft_validate( :sv_source_not_older_than_protonyms, set: :dates, name: 'Combination older than protonyms', description: 'The combination is older than protonyms in the combination' ) soft_validate( :sv_combination_linked_to_valid_name, set: :combination_linked_to_valid_name, fix: :sv_fix_combination_parent_update, name: 'Combination has valid parent', description: 'The combination should have the same parent as protonym' ) soft_validate( :sv_author_and_year_is_not_required, set: :year_is_not_required, fix: :sv_fix_author_and_year_is_not_required, name: 'Verbatim author and year are not required', description: 'Verbatim author and year are not required. The Fix will delete both' ) # @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)).where(sr['type'].in(::COMBINATION_TAXON_NAME_RELATIONSHIP_NAMES)) 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 def get_full_name return verbatim_name if verbatim_name.present? full_name end # Is this used before persistence of the complete Combination?! # Overrides {TaxonName#full_name_hash} # @return [Hash] # # Benchmark.measure { 1000.times do; Combination.find_by_id(ids.sample).full_name_hash; end} 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 # TODO: add higher classifcation here def combination_taxonomy d = full_name_hash protonyms_by_rank.to_a.first[1].ancestors.each do |a| d[a.rank] = a.name end d end def taxonomy(rebuild = false) if rebuild @taxonomy = combination_taxonomy else @taxonomy ||= combination_taxonomy end end # @return [Hash of {rank: Protonym}, nil] # the component names for this combination, sorted in order # _prior to it being saved_ def protonyms_by_rank result = {} if persisted? ar = APPLICABLE_RANKS.values # nX fewer calls to database than send() @protonyms_by_rank ||= TaxonNameRelationship::Combination .where(object_taxon_name: self) .eager_load(:subject_taxon_name) .sort{|a,b| ar.index(a.type) <=> ar.index(b.type)} .inject({}) {|hsh,n| hsh[n.rank_name] = n.subject_taxon_name; hsh} .to_h else APPLICABLE_RANKS.keys.each do |rank| if protonym = send(rank.to_sym) result[rank] = protonym end end @protonyms_by_rank = result end @protonyms_by_rank end # @return [Array of TaxonNames, nil] # return the component names for this combination prior to it being saved def protonyms_by_association APPLICABLE_RANKS.keys.collect{|r| self.send(r)}.compact end # @return [Array of TaxonName] # pre-ordered by rank def protonyms return protonyms_by_association if new_record? combination_taxon_names.sort{|a,b| RANKS.index(a.rank_string) <=> RANKS.index(b.rank_string) } 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 # @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.cached_nomenclature_date ? a.cached_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 protected def reset_protonyms_by_rank @protonyms_by_rank = nil end def validate_absence_of_subject_relationships if TaxonNameRelationship.where(subject_taxon_name_id: self.id).where("type NOT LIKE 'TaxonNameRelationship::CurrentCombination'").any? errors.add(:base, 'This combination could not be used as a Subject in any TaxonNameRelationships.') end end def sv_redundant_verbatim_name if verbatim_name == full_name protonyms_by_rank.values.each do |t| return true unless t.has_latinized_classification? end soft_validations.add(:verbatim_name, 'Verbatim name is provided but not needed, it is the same as the computed value.') end end 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_author_year:, project_id: self.project_id) soft_validations.add(:base, 'Combination is a duplicate') unless duplicate.empty? if a = Combination.matching_protonyms(get_full_name, **protonym_ids_params) soft_validations.add(:base, "Combination exists as protonym(s) with matching original combination: #{a.all.pluck(:cached).join(', ')}.") if a.any? end end def sv_combination_linked_to_valid_name check = protonyms.first if parent_id && check && check.parent_id && parent_id != check.parent_id soft_validations.add(:base, "Combination #{} should have the same parent and rank with #{check.}", success_message: 'The parent was updated', failure_message: 'The parent was not updated') end end def sv_fix_combination_parent_update check = protonyms.first if parent_id && check && check.parent_id && parent_id != check.parent_id begin TaxonName.transaction do # update_column(:parent_id, check.parent_id) ## do not use this, it breaks the taxon_name_hierarchies parent_id = check.parent_id self.save return true end rescue end end false end def sv_cached_names is_cached = true is_cached = false if != is_cached = false if != if is_cached && ( cached_is_valid.nil? || cached != get_full_name || cached_html != get_full_name_html || cached_nomenclature_date != nomenclature_date) is_cached = false end soft_validations.add( :base, 'Cached values should be updated', success_message: 'Cached values were updated', failure_message: 'Failed to update cached values') if !is_cached end def if !self.year_of_publication.nil? || !self..nil? soft_validations.add( :base, 'Verbatim author and year of publication are not required, they both derive from the source', success_message: 'Verbatim author and year were deleted', failure_message: 'Failed to delete verbatim author and year') end end def self.update_columns(year_of_publication: nil, verbatim_author: nil) return true end def sv_fix_redundant_verbatim_name self.update_columns(verbatim_name: nil) return true 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
72 73 74 |
# File 'app/models/combination.rb', line 72 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 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 |
# File 'app/models/combination.rb', line 58 class Combination < TaxonName # The ranks that can be used to build a Combination pointing to the corresponding TaxonNameRelationship type APPLICABLE_RANKS = %w{genus subgenus section subsection series subseries species subspecies variety subvariety form subform}.inject({}){|hsh,r| hsh[r] = "TaxonNameRelationship::Combination::#{r.capitalize}"; hsh; }.freeze before_validation :set_parent # Before we set cached ensure we draw current data after_save :reset_protonyms_by_rank validate :validate_absence_of_subject_relationships # TODO: make access private attr_accessor :disable_combination_relationship_check # Memoize. attr_accessor :protonyms_by_rank # 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.keys.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, inverse_of: :object_taxon_name 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 accepts_nested_attributes_for "#{rank}_taxon_name_relationship".to_sym, allow_destroy: true 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.present? 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_redundant_verbatim_name, set: :cached, fix: :sv_fix_redundant_verbatim_name, name: 'Redundant verbatim name', description: 'Verbatim name is present but identical to computed name') soft_validate( :sv_combination_duplicates, set: :combination_duplicates, name: 'Duplicate combination', description: 'Combination is a duplicate' ) soft_validate( :sv_year_of_publication_matches_source, set: :dates, name: 'Year of publication does not match the source', description: 'The published date of the combination is not the same as provided by the original publication' ) soft_validate( :sv_year_of_publication_not_older_than_protonyms, set: :dates, name: 'Varbatim year in combination older than in protonyms', description: 'The varbatim year in combination is older than in protonyms in the combination' ) soft_validate( :sv_source_not_older_than_protonyms, set: :dates, name: 'Combination older than protonyms', description: 'The combination is older than protonyms in the combination' ) soft_validate( :sv_combination_linked_to_valid_name, set: :combination_linked_to_valid_name, fix: :sv_fix_combination_parent_update, name: 'Combination has valid parent', description: 'The combination should have the same parent as protonym' ) soft_validate( :sv_author_and_year_is_not_required, set: :year_is_not_required, fix: :sv_fix_author_and_year_is_not_required, name: 'Verbatim author and year are not required', description: 'Verbatim author and year are not required. The Fix will delete both' ) # @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)).where(sr['type'].in(::COMBINATION_TAXON_NAME_RELATIONSHIP_NAMES)) 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 def get_full_name return verbatim_name if verbatim_name.present? full_name end # Is this used before persistence of the complete Combination?! # Overrides {TaxonName#full_name_hash} # @return [Hash] # # Benchmark.measure { 1000.times do; Combination.find_by_id(ids.sample).full_name_hash; end} 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 # TODO: add higher classifcation here def combination_taxonomy d = full_name_hash protonyms_by_rank.to_a.first[1].ancestors.each do |a| d[a.rank] = a.name end d end def taxonomy(rebuild = false) if rebuild @taxonomy = combination_taxonomy else @taxonomy ||= combination_taxonomy end end # @return [Hash of {rank: Protonym}, nil] # the component names for this combination, sorted in order # _prior to it being saved_ def protonyms_by_rank result = {} if persisted? ar = APPLICABLE_RANKS.values # nX fewer calls to database than send() @protonyms_by_rank ||= TaxonNameRelationship::Combination .where(object_taxon_name: self) .eager_load(:subject_taxon_name) .sort{|a,b| ar.index(a.type) <=> ar.index(b.type)} .inject({}) {|hsh,n| hsh[n.rank_name] = n.subject_taxon_name; hsh} .to_h else APPLICABLE_RANKS.keys.each do |rank| if protonym = send(rank.to_sym) result[rank] = protonym end end @protonyms_by_rank = result end @protonyms_by_rank end # @return [Array of TaxonNames, nil] # return the component names for this combination prior to it being saved def protonyms_by_association APPLICABLE_RANKS.keys.collect{|r| self.send(r)}.compact end # @return [Array of TaxonName] # pre-ordered by rank def protonyms return protonyms_by_association if new_record? combination_taxon_names.sort{|a,b| RANKS.index(a.rank_string) <=> RANKS.index(b.rank_string) } 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 # @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.cached_nomenclature_date ? a.cached_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 protected def reset_protonyms_by_rank @protonyms_by_rank = nil end def validate_absence_of_subject_relationships if TaxonNameRelationship.where(subject_taxon_name_id: self.id).where("type NOT LIKE 'TaxonNameRelationship::CurrentCombination'").any? errors.add(:base, 'This combination could not be used as a Subject in any TaxonNameRelationships.') end end def sv_redundant_verbatim_name if verbatim_name == full_name protonyms_by_rank.values.each do |t| return true unless t.has_latinized_classification? end soft_validations.add(:verbatim_name, 'Verbatim name is provided but not needed, it is the same as the computed value.') end end 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_author_year:, project_id: self.project_id) soft_validations.add(:base, 'Combination is a duplicate') unless duplicate.empty? if a = Combination.matching_protonyms(get_full_name, **protonym_ids_params) soft_validations.add(:base, "Combination exists as protonym(s) with matching original combination: #{a.all.pluck(:cached).join(', ')}.") if a.any? end end def sv_combination_linked_to_valid_name check = protonyms.first if parent_id && check && check.parent_id && parent_id != check.parent_id soft_validations.add(:base, "Combination #{} should have the same parent and rank with #{check.}", success_message: 'The parent was updated', failure_message: 'The parent was not updated') end end def sv_fix_combination_parent_update check = protonyms.first if parent_id && check && check.parent_id && parent_id != check.parent_id begin TaxonName.transaction do # update_column(:parent_id, check.parent_id) ## do not use this, it breaks the taxon_name_hierarchies parent_id = check.parent_id self.save return true end rescue end end false end def sv_cached_names is_cached = true is_cached = false if != is_cached = false if != if is_cached && ( cached_is_valid.nil? || cached != get_full_name || cached_html != get_full_name_html || cached_nomenclature_date != nomenclature_date) is_cached = false end soft_validations.add( :base, 'Cached values should be updated', success_message: 'Cached values were updated', failure_message: 'Failed to update cached values') if !is_cached end def if !self.year_of_publication.nil? || !self..nil? soft_validations.add( :base, 'Verbatim author and year of publication are not required, they both derive from the source', success_message: 'Verbatim author and year were deleted', failure_message: 'Failed to delete verbatim author and year') end end def self.update_columns(year_of_publication: nil, verbatim_author: nil) return true end def sv_fix_redundant_verbatim_name self.update_columns(verbatim_name: nil) return true 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 |
#protonyms_by_rank ⇒ Hash of {rank: Protonym}?
Returns the component names for this combination, sorted in order
_prior to it being saved_.
75 76 77 |
# File 'app/models/combination.rb', line 75 def protonyms_by_rank @protonyms_by_rank end |
Class Method Details
.find_by_protonym_ids(**keyword_args) ⇒ Scope
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 |
# File 'app/models/combination.rb', line 267 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
303 304 305 306 307 308 309 310 |
# File 'app/models/combination.rb', line 303 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.
255 256 257 258 259 260 261 262 263 |
# File 'app/models/combination.rb', line 255 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 = {}) ⇒ Protonym Scope
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 |
# File 'app/models/combination.rb', line 223 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)).where(sr['type'].in(::COMBINATION_TAXON_NAME_RELATIONSHIP_NAMES)) 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
439 440 441 442 443 444 445 |
# File 'app/models/combination.rb', line 439 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
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 |
# File 'app/models/combination.rb', line 448 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 |
#combination_taxonomy ⇒ Object
TODO: add higher classifcation here
363 364 365 366 367 368 369 370 |
# File 'app/models/combination.rb', line 363 def combination_taxonomy d = full_name_hash protonyms_by_rank.to_a.first[1].ancestors.each do |a| d[a.rank] = a.name end d end |
#composition ⇒ Object (protected)
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 |
# File 'app/models/combination.rb', line 600 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)
627 628 629 630 631 |
# File 'app/models/combination.rb', line 627 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.
433 434 435 |
# File 'app/models/combination.rb', line 433 def earliest_protonym_year publication_years.sort.first end |
#finest_protonym ⇒ Object
472 473 474 |
# File 'app/models/combination.rb', line 472 def finest_protonym protonyms_by_rank.values.last end |
#full_name_hash ⇒ Hash
Is this used before persistence of the complete Combination?! Overrides TaxonName#full_name_hash
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 |
# File 'app/models/combination.rb', line 329 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_full_name ⇒ Object
319 320 321 322 |
# File 'app/models/combination.rb', line 319 def get_full_name return verbatim_name if verbatim_name.present? full_name end |
#get_valid_taxon_name ⇒ Object
466 467 468 469 470 |
# File 'app/models/combination.rb', line 466 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.
314 315 316 317 |
# File 'app/models/combination.rb', line 314 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)
621 622 623 624 625 |
# File 'app/models/combination.rb', line 621 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
593 594 595 596 597 598 |
# File 'app/models/combination.rb', line 593 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 }`.
421 422 423 |
# File 'app/models/combination.rb', line 421 def protonym_ids_params protonyms_by_rank.inject({}) {|hsh, p| hsh.merge!( p[0].to_sym => p[1].id )} end |
#protonyms ⇒ Array of TaxonName
Returns pre-ordered by rank.
414 415 416 417 |
# File 'app/models/combination.rb', line 414 def protonyms return protonyms_by_association if new_record? combination_taxon_names.sort{|a,b| RANKS.index(a.rank_string) <=> RANKS.index(b.rank_string) } end |
#protonyms_by_association ⇒ Array of TaxonNames?
Return the component names for this combination prior to it being saved
408 409 410 |
# File 'app/models/combination.rb', line 408 def protonyms_by_association APPLICABLE_RANKS.keys.collect{|r| self.send(r)}.compact end |
#publication_years ⇒ Array of Integers
Returns the collective years the protonyms were (nomenclaturaly) published on (ordered from genus to below).
427 428 429 |
# File 'app/models/combination.rb', line 427 def publication_years description_years = protonyms.collect{|a| a.cached_nomenclature_date ? a.cached_nomenclature_date&.year : nil}.compact end |
#reset_protonyms_by_rank ⇒ Object (protected)
478 479 480 |
# File 'app/models/combination.rb', line 478 def reset_protonyms_by_rank @protonyms_by_rank = nil end |
#set_parent ⇒ Object (protected)
587 588 589 590 |
# File 'app/models/combination.rb', line 587 def set_parent names = protonyms write_attribute(:parent_id, names.first.parent.id) if names.count > 0 && names.first.parent end |
#sv_author_and_year_is_not_required ⇒ Object (protected)
568 569 570 571 572 573 574 575 |
# File 'app/models/combination.rb', line 568 def if !self.year_of_publication.nil? || !self..nil? soft_validations.add( :base, 'Verbatim author and year of publication are not required, they both derive from the source', success_message: 'Verbatim author and year were deleted', failure_message: 'Failed to delete verbatim author and year') end end |
#sv_cached_names ⇒ Object (protected)
550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 |
# File 'app/models/combination.rb', line 550 def sv_cached_names is_cached = true is_cached = false if != is_cached = false if != if is_cached && ( cached_is_valid.nil? || cached != get_full_name || cached_html != get_full_name_html || cached_nomenclature_date != nomenclature_date) is_cached = false end soft_validations.add( :base, 'Cached values should be updated', success_message: 'Cached values were updated', failure_message: 'Failed to update cached values') if !is_cached end |
#sv_combination_duplicates ⇒ Object (protected)
518 519 520 521 522 523 524 |
# File 'app/models/combination.rb', line 518 def sv_combination_duplicates duplicate = Combination.not_self(self).where(cached:, cached_author_year:, project_id: self.project_id) soft_validations.add(:base, 'Combination is a duplicate') unless duplicate.empty? if a = Combination.matching_protonyms(get_full_name, **protonym_ids_params) soft_validations.add(:base, "Combination exists as protonym(s) with matching original combination: #{a.all.pluck(:cached).join(', ')}.") if a.any? end end |
#sv_combination_linked_to_valid_name ⇒ Object (protected)
526 527 528 529 530 531 532 |
# File 'app/models/combination.rb', line 526 def sv_combination_linked_to_valid_name check = protonyms.first if parent_id && check && check.parent_id && parent_id != check.parent_id soft_validations.add(:base, "Combination #{} should have the same parent and rank with #{check.}", success_message: 'The parent was updated', failure_message: 'The parent was not updated') end end |
#sv_fix_author_and_year_is_not_required ⇒ Object (protected)
577 578 579 580 |
# File 'app/models/combination.rb', line 577 def self.update_columns(year_of_publication: nil, verbatim_author: nil) return true end |
#sv_fix_combination_parent_update ⇒ Object (protected)
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 |
# File 'app/models/combination.rb', line 534 def sv_fix_combination_parent_update check = protonyms.first if parent_id && check && check.parent_id && parent_id != check.parent_id begin TaxonName.transaction do # update_column(:parent_id, check.parent_id) ## do not use this, it breaks the taxon_name_hierarchies parent_id = check.parent_id self.save return true end rescue end end false end |
#sv_fix_redundant_verbatim_name ⇒ Object (protected)
582 583 584 585 |
# File 'app/models/combination.rb', line 582 def sv_fix_redundant_verbatim_name self.update_columns(verbatim_name: nil) return true end |
#sv_redundant_verbatim_name ⇒ Object (protected)
488 489 490 491 492 493 494 495 |
# File 'app/models/combination.rb', line 488 def sv_redundant_verbatim_name if verbatim_name == full_name protonyms_by_rank.values.each do |t| return true unless t.has_latinized_classification? end soft_validations.add(:verbatim_name, 'Verbatim name is provided but not needed, it is the same as the computed value.') end end |
#sv_source_not_older_than_protonyms ⇒ Object (protected)
504 505 506 507 508 509 510 |
# File 'app/models/combination.rb', line 504 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)
497 498 499 500 501 502 |
# File 'app/models/combination.rb', line 497 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)
512 513 514 515 516 |
# File 'app/models/combination.rb', line 512 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 |
#taxonomy(rebuild = false) ⇒ Object
372 373 374 375 376 377 378 |
# File 'app/models/combination.rb', line 372 def taxonomy(rebuild = false) if rebuild @taxonomy = combination_taxonomy else @taxonomy ||= combination_taxonomy end end |
#validate_absence_of_subject_relationships ⇒ Object (protected)
482 483 484 485 486 |
# File 'app/models/combination.rb', line 482 def validate_absence_of_subject_relationships if TaxonNameRelationship.where(subject_taxon_name_id: self.id).where("type NOT LIKE 'TaxonNameRelationship::CurrentCombination'").any? errors.add(:base, 'This combination could not be used as a Subject in any TaxonNameRelationships.') end end |