SpeciesFileGroup/taxonworks

View on GitHub
app/models/concerns/shared/citations.rb

Summary

Maintainability
A
0 mins
Test Coverage
# Shared code for Citations.
#
#  The default behaviour with order by youngest source and oldest source is to place records with NIL *last*
# in the list.
#  When multiple citations exist the earliest or latest is used in the sort order.
#
module Shared::Citations
  extend ActiveSupport::Concern

  included do
    related_class = self.name
    related_table_name = self.table_name

    Citation.related_foreign_keys.push self.name.foreign_key

    # !! Validate: true assigns housekeeping where needed (!don't make this self-referential!)
    has_many :citations, as: :citation_object, dependent: :destroy, inverse_of: :citation_object, validate: true
    has_many :citation_topics, through: :citations, validate: true
    has_many :topics, through: :citation_topics, validate: true

    has_many :subsequent_citations, -> { where(is_original: nil) }, as: :citation_object, class_name: 'Citation'

    has_many :sources, -> { distinct }, through: :citations, inverse_of: :citations
    has_many :subsequent_sources, -> { distinct },  through: :subsequent_citations, source: :source

    has_one :origin_citation, -> {where(is_original: true)}, as: :citation_object, class_name: 'Citation', inverse_of: :citation_object

    has_one :source, through: :origin_citation, inverse_of: :origin_citations

    scope :without_citations, -> {includes(:citations).where(citations: {id: nil})}

    # scope :order_by_youngest_source_first, -> {
    #  joins("LEFT OUTER JOIN citations on #{related_table_name}.id = citations.citation_object_id
    # LEFT OUTER JOIN sources ON citations.source_id = sources.id").
    #  group("#{related_table_name}.id").order("MAX(COALESCE(sources.cached_nomenclature_date,
    # Date('1-1-0001'))) DESC").
    #  where("((citations.citation_object_type = '#{related_class}') OR (citations.citation_object_type is null))")
    # }

    scope :order_by_youngest_source_first, -> {
      join_str = ActiveRecord::Base.send(
        :sanitize_sql_array,
        ["LEFT OUTER JOIN citations ON #{related_table_name}.id = " \
         'citations.citation_object_id ' \
         'AND citations.citation_object_type = ? LEFT OUTER JOIN sources ' \
         'ON citations.source_id = sources.id',
         related_class])
      joins(join_str).group("#{related_table_name}.id")
        .order(Arel.sql("MAX(COALESCE(sources.cached_nomenclature_date, Date('1-1-0001'))) DESC"))
    }

    # SEE https://github.com/rails/arel/issues/399 for issue with ordering by named function
    #define_singleton_method "order_by_youngest_source_first" do
    #  d =  Arel::Attribute.new(Arel::Table.new(:sources), :cached_nomenclature_date)
    #  r  = Arel::Attribute.new(Arel::Table.new(related_table_name), :id)
    #  f1 = Arel::Nodes::NamedFunction.new('Now', [] )
    #  func = Arel::Nodes::NamedFunction.new('COALESCE', [d, f1])

    #  # Fails with bind error, maybe real bug
    #  #  inner_joins = joins(:citations,  :sources).arel.join_sources
    #  #  left_joins = inner_joins.map do |join|
    #  #    Arel::Nodes::OuterJoin.new(join.left, join.right)
    #  #  end

    #  joins("LEFT OUTER JOIN citations on #{related_table_name}.id = citations.citation_object_id
    # LEFT OUTER JOIN sources ON citations.source_id = sources.id").
    #    where("citations.citation_object_type = '#{related_class}'").
    #    group(r).
    #    order(func.desc)
    # end

    define_singleton_method 'order_by_oldest_source_first' do
      d  = Arel::Attribute.new(Arel::Table.new(:sources), :cached_nomenclature_date)
      r  = Arel::Attribute.new(Arel::Table.new(related_table_name), :id)
      f1 = Arel::Nodes::NamedFunction.new('Now', [])

      func  = Arel::Nodes::NamedFunction.new('COALESCE', [d, f1])
      func2 = Arel::Nodes::NamedFunction.new('min', [func])

      # Fails with bind error, maybe real bug with AREL, PSQL
      #  inner_joins = joins(:citations,  :sources).arel.join_sources
      #  left_joins = inner_joins.map do |join|
      #    Arel::Nodes::OuterJoin.new(join.left, join.right)
      #  end

      # was
      #      joins("LEFT OUTER JOIN citations ON #{related_table_name}.id = citations.citation_object_id LEFT OUTER
      # JOIN sources ON citations.source_id = sources.id").
      #      where("citations.citation_object_type = '#{related_class}' OR citations.citation_object_type is null").
      join_str = ActiveRecord::Base.send(
        :sanitize_sql_array,
        ["LEFT OUTER JOIN citations ON #{related_table_name}.id = " \
         'citations.citation_object_id ' \
         'AND citations.citation_object_type = ? LEFT OUTER JOIN sources ' \
         'ON citations.source_id = sources.id',
         related_class])
      joins(join_str).group(r).order(func2)
    end

    accepts_nested_attributes_for :citations, reject_if: :reject_citations, allow_destroy: true
    accepts_nested_attributes_for :origin_citation, reject_if: :reject_citations, allow_destroy: true

    validate :origin_citation_source_id, if: -> { !new_record? }

    # !! use validate: true in associations settings to trigger this as needed
    # Required to trigger validate callbacks, which in turn set user_id related housekeeping
    # validates_associated :citations
  end

  class_methods do
    def oldest_by_citation
      order_by_oldest_source_first.to_a.first
    end

    def youngest_by_citation
      order_by_youngest_source_first.to_a.first
    end
  end

  # @return [Date, nil]
  # !! Over-riden in various places, but it shouldn't be
  # See Source::Bibtex for context as to how this is built.
  #
  def nomenclature_date
    self.class.joins(citations: [:source])
    .where(citations: {citation_object: self, is_original: true})
    .select('sources.cached_nomenclature_date')
    .first&.cached_nomenclature_date
  end

  alias_method :source_nomenclature_date, :nomenclature_date

  def origin_citation_source_id
    if origin_citation && origin_citation.source_id.blank?
      errors.add(:base, 'the origin citation must have a source')
    end
  end

  def sources_by_topic_id(topic_id)
    Source.joins(:citation_topics).where(citations: {citation_object: self}, citation_topics: {topic_id:})
  end

  # @return [Boolean]
  #   if at least one citation is required override this with true in including class
  def requires_citation?
    false
  end

  def cited?
    self.citations.any?
  end

  def mark_citations_for_destruction
    citations.map(&:mark_for_destruction)
  end

  protected

  def reject_citations(attributed)
    if (attributed['source_id'].blank? && attributed['source'].blank?)
      return true if new_record?
      return true if attributed['pages'].blank?
    end
    false
  end

end