diff --git a/app/models/person.rb b/app/models/person.rb index c78f4de1f4..1e87ef08fc 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -26,7 +26,7 @@ class Person < ActiveRecord::Base has_many :collecting_events, through: :collector_roles, source: :role_object, source_type: 'CollectingEvent' has_many :taxon_determinations, through: :determiner_roles, source: :role_object, source_type: 'TaxonDetermination' has_many :taxon_name_authors, through: :taxon_name_author_roles, source: :role_object, source_type: 'TaxonName' - has_many :type_specimens, through: :type_designator_roles, source: :role_object, source_type: 'TypeSpecimen' + has_many :type_material, through: :type_designator_roles, source: :role_object, source_type: 'TypeMaterial' #scope :named, -> (name) {where(name: name)} #scope :named_smith, where(last_name: 'Smith') diff --git a/app/models/protonym.rb b/app/models/protonym.rb index 62c0e72d38..4d81b2ede4 100644 --- a/app/models/protonym.rb +++ b/app/models/protonym.rb @@ -13,6 +13,8 @@ class Protonym < TaxonName where("taxon_name_relationships.type LIKE 'TaxonNameRelationship::OriginalCombination::%'") }, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id + has_many :type_material + # subject object # Aus original_genus of bus # aus type_species of Bus @@ -45,7 +47,6 @@ class Protonym < TaxonName has_one d.inverse_assignment_method.to_sym, through: relationship, source: :subject_taxon_name end end - end scope :named, -> (name) {where(name: name)} @@ -62,7 +63,6 @@ class Protonym < TaxonName scope :with_taxon_name_classification_array, -> (taxon_name_class_name_base_array) { includes(:taxon_name_classifications).where('taxon_name_classifications.type in (?)', taxon_name_class_name_base_array).references(:taxon_name_classifications) } scope :without_taxon_name_classification, -> (taxon_name_class_name) { where('id not in (SELECT taxon_name_id FROM taxon_name_classifications WHERE type LIKE ?)', "#{taxon_name_class_name}")} scope :without_taxon_name_classification_array, -> (taxon_name_class_name_array) { where('id not in (SELECT taxon_name_id FROM taxon_name_classifications WHERE type in (?))', taxon_name_class_name_array) } - scope :without_taxon_name_classifications, -> { includes(:taxon_name_classifications).where(taxon_name_classifications: {taxon_name_id: nil}) } scope :that_is_valid, -> { @@ -84,6 +84,50 @@ class Protonym < TaxonName :validate_source_type, :new_parent_taxon_name + + + def list_of_coordinated_names + if self.incorrect_original_spelling.nil? + search_rank = NomenclaturalRank::Iczn.group_base(self.rank_string) + if !!search_rank + if search_rank =~ /Family/ + z = Protonym.family_group_base(self.name) + search_name = z.nil? ? nil : NomenclaturalRank::Iczn::FamilyGroup::ENDINGS.collect{|i| z+i} + #search_name = z.nil? ? nil : "#{z}(ini|ina|inae|idae|oidae|odd|ad|oidea)" + else + search_name = self.name + end + else + search_name = nil + end + + unless search_name.nil? + list = Protonym.ancestors_and_descendants_of(self). + with_rank_class_including(search_rank). + with_name_in_array(search_name). + as_subject_without_taxon_name_relationship_base('TaxonNameRelationship::Iczn::Invalidating::Synonym') # <- use this + #list1 = self.ancestors_and_descendants # scope with parens + #list1 = list1.select{|i| /#{search_rank}.*/.match(i.rank_class.to_s)} # scope on rank_class + #list1 = list1.select{|i| /#{search_name}/.match(i.name)} # scope on named + #list1 = list1.reject{|i| i.unavailable_or_invalid?} # scope with join on taxon_name_relationships and where > 1 on them + else + list = [] + end + else + list = [self.incorrect_original_spelling.object_taxon_name] + end + return list + end + + def ancestors_and_descendants + Protonym.ancestors_and_descendants_of(self).to_a + end + + def self.family_group_base(name_string) + name_string.match(/(^.*)(ini|ina|inae|idae|oidae|odd|ad|oidea)/) + $1 + end + protected def incorrect_original_spelling @@ -221,49 +265,6 @@ def sv_fix_coordinated_names return fixed end - def list_of_coordinated_names - - if self.incorrect_original_spelling.nil? - search_rank = NomenclaturalRank::Iczn.group_base(self.rank_string) - if !!search_rank - if search_rank =~ /Family/ - z = Protonym.family_group_base(self.name) - search_name = z.nil? ? nil : NomenclaturalRank::Iczn::FamilyGroup::ENDINGS.collect{|i| z+i} - #search_name = z.nil? ? nil : "#{z}(ini|ina|inae|idae|oidae|odd|ad|oidea)" - else - search_name = self.name - end - else - search_name = nil - end - - unless search_name.nil? - list = Protonym.ancestors_and_descendants_of(self). - with_rank_class_including(search_rank). - with_name_in_array(search_name). - as_subject_without_taxon_name_relationship_base('TaxonNameRelationship::Iczn::Invalidating::Synonym') # <- use this - #list1 = self.ancestors_and_descendants # scope with parens - #list1 = list1.select{|i| /#{search_rank}.*/.match(i.rank_class.to_s)} # scope on rank_class - #list1 = list1.select{|i| /#{search_name}/.match(i.name)} # scope on named - #list1 = list1.reject{|i| i.unavailable_or_invalid?} # scope with join on taxon_name_relationships and where > 1 on them - else - list = [] - end - else - list = [self.incorrect_original_spelling.object_taxon_name] - end - return list - end - - def ancestors_and_descendants - Protonym.ancestors_and_descendants_of(self).to_a - end - - def self.family_group_base(name_string) - name_string.match(/(^.*)(ini|ina|inae|idae|oidae|odd|ad|oidea)/) - $1 - end - def sv_type_placement # type of this taxon is not included in this taxon if !!self.type_taxon_name diff --git a/app/models/taxon_name.rb b/app/models/taxon_name.rb index 8f2dee162f..216fe18b8c 100644 --- a/app/models/taxon_name.rb +++ b/app/models/taxon_name.rb @@ -1,4 +1,3 @@ - class TaxonName < ActiveRecord::Base include Housekeeping @@ -9,7 +8,6 @@ class TaxonName < ActiveRecord::Base acts_as_nested_set scope: [:project_id] belongs_to :source - has_many :taxon_name_classifications #relationships as a subject @@ -66,12 +64,6 @@ class TaxonName < ActiveRecord::Base where('tnr1.subject_taxon_name_id IS NULL AND tnr2.object_taxon_name_id IS NULL') } - soft_validate(:sv_missing_fields, set: :missing_fields) - soft_validate(:sv_parent_is_valid_name, set: :parent_is_valid_name) - soft_validate(:sv_source_older_then_description, set: :source_older_then_description) - - - validates_presence_of :type, message: 'Type is not specified' validates_presence_of :rank_class, message: 'Rank is a required field', if: Proc.new { |tn| [Protonym].include?(tn.class)} validates_presence_of :name, message: 'Name is a required field', if: Proc.new { |tn| [Protonym].include?(tn.class)} @@ -83,11 +75,14 @@ class TaxonName < ActiveRecord::Base :validate_parent_is_set, :check_new_rank_class, :check_new_parent_class, - :validate_source_type + :validate_source_type, + :set_cached_name, + :set_cached_author_year, + :set_cached_higher_classification - before_validation :set_cached_name, - :set_cached_author_year, - :set_cached_higher_classification + soft_validate(:sv_missing_fields, set: :missing_fields) + soft_validate(:sv_parent_is_valid_name, set: :parent_is_valid_name) + soft_validate(:sv_source_older_then_description, set: :source_older_then_description) def all_taxon_name_relationships # (self.taxon_name_relationships & self.related_taxon_name_relationships) diff --git a/app/models/type_designator.rb b/app/models/type_designator.rb index 2f740ce67f..9aea0ae410 100644 --- a/app/models/type_designator.rb +++ b/app/models/type_designator.rb @@ -1,4 +1,3 @@ class TypeDesignator < Role::ProjectRole - include Housekeeping end diff --git a/app/models/type_material.rb b/app/models/type_material.rb new file mode 100644 index 0000000000..fe03a1cd03 --- /dev/null +++ b/app/models/type_material.rb @@ -0,0 +1,40 @@ +class TypeMaterial < ActiveRecord::Base + belongs_to :source + + include Housekeeping + include Shared::Citable + + ICZN_TYPES = { + 'holotype' => Specimen, + 'paratype' => Specimen, + 'neotype' => Specimen, + 'lectotype' => Specimen, + 'paratypes' => Lot, + 'syntypes' => Lot + } + + ICN_TYPES = {} + + belongs_to :material, foreign_key: :biological_object_id, class_name: 'CollectionObject' + belongs_to :protonym + has_many :type_designator_roles, class_name: 'TypeDesignator', as: :role_object + has_many :type_designators, through: :type_designator_roles, source: :person + + validates :protonym, presence: true + validates :material, presence: true + validates_presence_of :type_type + + validate :check_type_type + + protected + + def check_type_type + if self.protonym + code = self.protonym.rank_class.nomenclatural_code + if (code == :iczn && !ICZN_TYPES.keys.include?(self.type_type)) || (code == :icn && !ICN_TYPES.keys.include?(self.type_type)) + errors.add(:type_type, 'Not a legal type for the nomenclatural code provided') + end + end + end + +end diff --git a/app/models/type_specimen.rb b/app/models/type_specimen.rb deleted file mode 100644 index 0b964fc6e4..0000000000 --- a/app/models/type_specimen.rb +++ /dev/null @@ -1,12 +0,0 @@ -class TypeSpecimen < ActiveRecord::Base - - include Housekeeping - include Shared::Citable - - belongs_to :biological_object - belongs_to :taxon_name - - has_many :type_designator_roles, class_name: 'TypeDesignator', as: :role_object - has_many :type_designators, through: :type_designator_roles, source: :person - -end diff --git a/db/migrate/20140128221223_create_type_materials.rb b/db/migrate/20140128221223_create_type_materials.rb new file mode 100644 index 0000000000..ba4bb5b150 --- /dev/null +++ b/db/migrate/20140128221223_create_type_materials.rb @@ -0,0 +1,15 @@ +class CreateTypeMaterials < ActiveRecord::Migration + def change + create_table :type_materials do |t| + t.integer :protonym_id + t.integer :biological_object_id + t.string :type_type + t.references :source, index: true + t.integer :created_by_id + t.integer :updated_by_id + t.integer :project_id + + t.timestamps + end + end +end diff --git a/db/migrate/20140128221713_drop_type_specimen_table.rb b/db/migrate/20140128221713_drop_type_specimen_table.rb new file mode 100644 index 0000000000..dbaf13480c --- /dev/null +++ b/db/migrate/20140128221713_drop_type_specimen_table.rb @@ -0,0 +1,5 @@ +class DropTypeSpecimenTable < ActiveRecord::Migration + def change + drop_table :type_specimens + end +end diff --git a/spec/factories/type_material_factory.rb b/spec/factories/type_material_factory.rb new file mode 100644 index 0000000000..446922a60c --- /dev/null +++ b/spec/factories/type_material_factory.rb @@ -0,0 +1,10 @@ +FactoryGirl.define do + factory :type_material, traits: [:housekeeping] do + factory :valid_type_material do + type_type 'holotype' + association :protonym, factory: :iczn_species + association :material, factory: :valid_specimen + end + end +end + diff --git a/spec/factories/type_specimen_factory.rb b/spec/factories/type_specimen_factory.rb deleted file mode 100644 index 579097abbc..0000000000 --- a/spec/factories/type_specimen_factory.rb +++ /dev/null @@ -1,9 +0,0 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - -FactoryGirl.define do - factory :type_specimen, traits: [:housekeeping] do - biological_object nil - taxon_name nil - type_type "MyString" - end -end diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb index f07cd81973..ecd2240339 100644 --- a/spec/models/person_spec.rb +++ b/spec/models/person_spec.rb @@ -69,7 +69,7 @@ end specify 'type_designations' do - expect(person).to respond_to(:type_specimens) + expect(person).to respond_to(:type_material) end end @@ -177,9 +177,9 @@ specify 'is_type_designator?' do expect(@vp).to respond_to(:is_type_designator?) expect(@vp.is_type_designator?).to be_false - type_specimen = FactoryGirl.create(:type_specimen) - type_specimen.type_designators << @vp - type_specimen.save! + type_material = FactoryGirl.create(:valid_type_material) + type_material.type_designators << @vp + type_material.save! @vp.reload expect(@vp.is_type_designator?).to be_true end diff --git a/spec/models/type_material_spec.rb b/spec/models/type_material_spec.rb new file mode 100644 index 0000000000..52fb39e335 --- /dev/null +++ b/spec/models/type_material_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' + +describe TypeMaterial do + let(:type_material) { FactoryGirl.build(:type_material) } + + context 'associations' do + context 'belongs to' do + specify 'protonym' do + expect(type_material).to respond_to(:protonym) + end + specify 'material' do + expect(type_material).to respond_to(:material) + end + specify 'source' do + expect(type_material).to respond_to(:source) + end + end + end + + context 'validations' do + context 'require' do + before(:each) { + type_material.valid? + } + specify 'protonym' do + expect(type_material.errors.include?(:protonym)).to be_true + end + + specify 'material' do + expect(type_material.errors.include?(:material)).to be_true + end + + specify 'type_type' do + expect(type_material.errors.include?(:type_type)).to be_true + end + end + + context 'Protonym restrictions and linkages' do + before(:each) { + @iczn_type = FactoryGirl.build(:type_material, protonym: FactoryGirl.build(:iczn_species)) + @icn_type = FactoryGirl.build(:type_material, protonym: FactoryGirl.build(:icn_species)) + } + + specify 'type_type is one of ICZN_TYPES.keys for ICZN name' do + @iczn_type.type_type = 'foo' + @iczn_type.valid? + expect(@iczn_type.errors.include?(:type_type)).to be_true + expect(@iczn_type.errors.messages[:type_type]).to eq(['Not a legal type for the nomenclatural code provided']) + @iczn_type.type_type = 'holotype' + @iczn_type.valid? + expect(@iczn_type.errors.include?(:type_type)).to be_false + end + + specify 'type_type is one of ICN_TYPES.keys for ICN name' do + pending + end + end + + context 'Material restrictions' do + before(:each) { + type_material.protonym = FactoryGirl.build(:iczn_species) + } + + specify 'type_type restricts the BiologicalObject subclass to an _TYPES.value' do + end + + specify 'collection_object is a BiologicalCollectionObject' do + end + end + + context 'type_type restrictions' do + pending 'only one of holotype, lectotype, neotype per iczn species' + end + + end + + context 'methods' do + before(:each) { + iczn_type = FactoryGirl.build(:valid_type_material) + icn_type = FactoryGirl.build(:valid_type_material, protonym: FactoryGirl.build(:icn_species)) + } + pending 'Source is the Protonym source when not provided locally.' + pending 'TypeDesignator role(s) should be possible when a specific person needs to be identified as the person who designated the type' + pending 'a source citation can identify (override the source_id in Protonym) where the type designation was made' + end + + context 'soft validation' do + pending 'no source provided if self.source.nil && self.protonym.source.nil?' + end + +end + diff --git a/spec/models/type_specimen_spec.rb b/spec/models/type_specimen_spec.rb deleted file mode 100644 index f254175549..0000000000 --- a/spec/models/type_specimen_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' - -describe TypeSpecimen do - let(:type_specimen) { TypeSpecimen.new } - - context 'associations' do - context 'belongs to' do - - specify 'taxon_name' do - expect(type_specimen).to respond_to(:taxon_name) - end - - specify 'biological_object' do - expect(type_specimen).to respond_to(:biological_object) - end - end - end - - context 'validations' do - pending 'type_type should be defined in a constant for iczn' - pending 'type_type should be defined in a constant for icn' - pending 'taxon_name should always be type Protonym' - end - - context 'methods/options' do - pending 'a constant should relate type_type to valid biological_object subclass' do - # for example {holotype: Specimen, syntype: Specimen, syntypes: Lot, paratypes: Lot, paratype: Specimen} - end - pending 'TypeDesignator role(s) should be possible when a specific person needs to be identified as the person who designated the type' - pending 'a source citation can identify where the type designation was made' - end - -end