app/models/citation.rb
# A Citation is an assertion that the subject (i.e. citation object/record/data instance),
# or some attribute of it, was referenced or originated in a Source.
#
# @!attribute citation_object_type
# @return [String]
# Rails STI, the class of the object being cited
#
# @!attribute citation_object_id
# @return [Integer]
# Rails STI, the id of the object being cited
#
# @!attribute source_id
# @return [Integer]
# the source ID
#
# @!attribute project_id
# @return [Integer]
# the project ID
#
# @!attribute pages
# @return [String, nil]
# a specific location/localization for the data in the Source, if you lead with an integer separated by space or punctation that
# integer will be returned as the "first" page and usable in direct linkouts to Documents if available
#
# @!attribute is_original
# @return [Boolean]
# is this the first citation in which the data were observed?
#
class Citation < ApplicationRecord
# Citations do not have Confidence or DataAttribute.
include Housekeeping
include Shared::Notes
include Shared::Tags
include Shared::IsData
include Shared::PolymorphicAnnotator
include SoftValidation
attr_accessor :no_cached
polymorphic_annotates('citation_object')
# belongs_to :source, inverse_of: :origin_citations
belongs_to :source, inverse_of: :citations
has_many :citation_topics, inverse_of: :citation, dependent: :destroy
has_many :topics, through: :citation_topics, inverse_of: :citations
has_many :documents, through: :source
validates_presence_of :source
validates_uniqueness_of :source_id, scope: [:citation_object_id, :citation_object_type, :pages]
validates_uniqueness_of :is_original, scope: [:citation_object_type, :citation_object_id], message: 'origin can only be assigned once', allow_nil: true, if: :is_original?
accepts_nested_attributes_for :citation_topics, allow_destroy: true, reject_if: :reject_citation_topics
accepts_nested_attributes_for :topics, allow_destroy: true, reject_if: :reject_topic
before_destroy :prevent_if_required
after_create :add_source_to_project
before_save {@old_is_original = is_original_was}
before_save {@old_citation_object_id = citation_object_id_was}
before_save {@old_source_id = source_id_was}
after_save :update_related_cached_values, if: :is_original?
after_save :set_cached_names_for_taxon_names, unless: -> {self.no_cached}
after_destroy :set_cached_names_for_taxon_names, unless: -> {self.no_cached}
soft_validate(:sv_page_range, set: :page_range)
def self.batch_create(params)
ids = params[:citation_object_id]
params.delete(:citation_object_id)
citations = []
Citation.transaction do
begin
ids.each do |id|
citations.push Citation.create!(
params.merge(
citation_object_id: id
)
)
end
rescue ActiveRecord::RecordInvalid
return false
end
end
citations
end
# TODO: deprecate
# @return [Scope of matching sources]
def self.find_for_autocomplete(params)
term = params['term']
ending = term + '%'
wrapped = '%' + term + '%'
joins(:source).where('sources.cached ILIKE ? OR sources.cached ILIKE ? OR citation_object_type LIKE ?', ending, wrapped, ending).with_project_id(params[:project_id])
end
# @return [Boolean]
# true if is_original is checked, false if nil/false
def is_original?
is_original ? true : false
end
# @return [String, nil]
# the first integer in the string, as a string
def first_page
/(?<i>\d+)/ =~ pages
i
end
# @return [Integer, nil]
# if a target document
def target_document_page
target_document.try(:pdf_page_for, first_page).try(:first)
end
# @return [Document, nil]
def target_document
documents.order('documentation.position').first
end
protected
def add_source_to_project
!!ProjectSource.find_or_create_by(project:, source:)
end
def reject_citation_topics(attributed)
attributes['id'].blank? && attributed['topic_id'].blank? && attributed['topic'].blank? && attributed['topic_attributes'].blank?
end
def reject_topic(attributed)
attributed['name'].blank? || attributed['definition'].blank?
end
def update_related_cached_values
if is_original != @old_is_original || citation_object_id != @old_citation_object_id || source_id != @old_source_id
if citation_object_type == 'TaxonName'
citation_object.update_columns(
cached_author_year: citation_object.get_author_and_year,
cached_nomenclature_date: citation_object.nomenclature_date) if citation_object.persisted?
end
end
true
end
# TODO: modify for asserted distributions and other origin style relationships
def prevent_if_required
unless citation_object && citation_object.respond_to?(:ignore_citation_restriction) && citation_object.ignore_citation_restriction
if !marked_for_destruction? && !new_record? && citation_object.requires_citation? && citation_object.citations.count == 1
errors.add(:base, 'at least one citation is required')
throw :abort
end
end
end
def set_cached_names_for_taxon_names
if is_original != @old_is_original || citation_object_id != @old_citation_object_id || source_id != @old_source_id
if citation_object_type == 'TaxonNameRelationship' && TAXON_NAME_RELATIONSHIP_NAMES_INVALID.include?(citation_object.try(:type_name))
begin
TaxonNameRelationship.transaction do
t = citation_object.subject_taxon_name
vn = t.get_valid_taxon_name
t.update_columns(
cached: t.get_full_name,
cached_html: t.get_full_name_html,
cached_valid_taxon_name_id: vn.id)
# @proceps: This and below is not updating cached names. Is this required because timing (new dates) may change synonymy?
t.combination_list_self.each do |c|
c.update_column(:cached_valid_taxon_name_id, vn.id)
end
vn.list_of_invalid_taxon_names.each do |s|
s.update_column(:cached_valid_taxon_name_id, vn.id)
s.combination_list_self.each do |c|
c.update_column(:cached_valid_taxon_name_id, vn.id)
end
end
end
rescue ActiveRecord::RecordInvalid
raise
end
false
end
end
end
def sv_page_range
if pages.blank?
soft_validations.add(:pages, 'Citation pages are not provided')
elsif source.pages.present?
matchdata1 = pages.match(/(\d+) ?[-–] ?(\d+)|(\d+)/)
if matchdata1
citMinP = matchdata1[1] ? matchdata1[1].to_i : matchdata1[3].to_i
citMaxP = matchdata1[2] ? matchdata1[2].to_i : matchdata1[3].to_i
matchdata = source.pages.match(/(\d+) ?[-–] ?(\d+)|(\d+)/)
if citMinP && citMaxP && matchdata
minP = matchdata[1] ? matchdata[1].to_i : matchdata[3].to_i
maxP = matchdata[2] ? matchdata[2].to_i : matchdata[3].to_i
minP = 1 if minP == maxP && %w{book booklet manual mastersthesis phdthesis techreport}.include?(source.bibtex_type)
unless (maxP && minP && minP <= citMinP && maxP >= citMaxP)
soft_validations.add(:pages, 'Citation is out of the source page range')
end
end
end
end
end
end