SpeciesFileGroup/taxonworks

View on GitHub
app/models/serial.rb

Summary

Maintainability
A
35 mins
Test Coverage
# Serial - represents a journal or other serial (repeated) publication. It follows the ISSN model for serials.
#
# @!attribute place_published
#   @return [String]
#     The name of the place(s) where the serial is published.
#
# @!attribute primary_language_id
#   @return [Integer]
#   The id of the Language - language of this serial.  According to the ISSN a new ISSN is minted for a journal that
#     changes languages.
#
# @!attribute first_year_of_issue
#   @return [Integer]
#     the first year this serial was published
#
# @!attribute last_year_of_issue
#   @return [Integer]
#     the last year this serial was published
#
# @!attribute translated_from_serial_id
#   @return [Integer]
#     the id of the serial that this serial is a direct translation of
#
# @!attribute publisher
#   @return [String]
#    the serial publisher
#
# @!attribute name
#   @return [String]
#     the name of the serial
#
# TODO handle translations (which are simultaneous)
class Serial < ApplicationRecord

  include Housekeeping::Users
  include Housekeeping::Timestamps
  include Shared::AlternateValues
  include Shared::DataAttributes
  include Shared::Notes
  include Shared::Identifiers
  include Shared::Tags
  include Shared::IsData
  include SoftValidation
  include Shared::SharedAcrossProjects
  include Shared::HasPapertrail

  ALTERNATE_VALUES_FOR = [:name, :publisher, :place_published].freeze

  belongs_to :translated_from_serial, foreign_key: :translated_from_serial_id, class_name: 'Serial'
  belongs_to :language, foreign_key: :primary_language_id

  has_many :sources, class_name: 'Source::Bibtex', inverse_of: :serial, dependent: :restrict_with_error
  has_many :translations, foreign_key: :translated_from_serial_id, class_name: 'Serial', dependent: :destroy

  has_many :succeeding_serial_chronologies, foreign_key: :succeeding_serial_id, class_name: 'SerialChronology', dependent: :restrict_with_error
  has_many :preceding_serial_chronologies, foreign_key: :preceding_serial_id, class_name: 'SerialChronology', dependent: :restrict_with_error

  # Single preceding chronology will be multiple serials if there is a merge
  has_many :immediately_preceding_serials, through: :succeeding_serial_chronologies, source: :preceding_serial

  # Single succeeding chronology will be multiple serials if there is a split
  has_many :immediately_succeeding_serials, through: :preceding_serial_chronologies, source: :succeeding_serial

  accepts_nested_attributes_for :alternate_values, reject_if: lambda { |av| av[:value].blank? }, allow_destroy: true

  validates_presence_of :name

  soft_validate(:sv_duplicate?)

  # @param [String] compared_string
  # @param [String] column
  # @param [Integer] limit
  # @return [Scope]
  #   Levenshtein calculated related records per supplied column
  def nearest_by_levenshtein(compared_string = nil, column = 'name', limit = 10)
    return Serial.none if compared_string.blank?

    # Levenshtein in postgres requires all strings be 255 or fewer
    order_str = Serial.send(
      :sanitize_sql_for_conditions,
      ["levenshtein(Substring(serials.#{column} from 0 for 250), ?)",
       compared_string[0..250]])

    Serial.where('id <> ?', self.to_param)
      .order(Arel.sql(order_str))
      .limit(limit)
  end

  # @return [Boolean]
  #   is there another serial with the same name?  Also checkes alternate values.
  def duplicate?
    # ret_val = false
    if self.new_record?
      ret_val = Serial.exists?(name: self.name)
    else
      name_str = ActiveRecord::Base.send(
        :sanitize_sql_array,
        ['name = ? AND NOT (id = ?)',
         Utilities::Strings.escape_single_quote(self.name),
         self.id])
      ret_val  = Serial.where(name_str).to_a.size > 0
    end

    if ret_val == false
      # check if there is another alternate value with the same name
      a = Serial.with_alternate_value_on(:name, self.name)
      # select alternate value based on alternate_value_object class, alternate_value_object_attribute(column) & value
      if a.count > 0
        ret_val = true
      end
    end
    ret_val
  end

=begin
  def full_chronology
    # return ordered array of serials associated with this serial
  end
=end

  # @param [Serial] start_serial
  # @return [Array]
  def all_previous(start_serial = self)
    # provides an array of all previous incarnations of me

    out_array = []
    start_serial.immediately_preceding_serials.order(:name).each do |serial|
      out_array.push(serial)
      prev = all_previous(serial)

      out_array.push(prev) unless prev.empty?
    end
    return out_array
  end

  # @param [Serial] start_serial
  # @return [Array]
  def all_succeeding(start_serial = self)
    # provides an array of all succeeding incarnations of me
    out_array = []
    start_serial.immediately_succeeding_serials.order(:name).each do |serial|
      out_array.push(serial)
      succeeding = all_succeeding(serial)

      out_array.push(succeeding) unless succeeding.empty?
    end
    out_array
  end

  def self.used_recently(user_id)
    t = Source.arel_table
    p = Serial.arel_table

    # i is a select manager
    i = t.project(t['serial_id'], t['updated_at']).from(t)
      .where(t['updated_at'].gt(1.months.ago))
      .where(t['updated_by_id'].eq(user_id))
      .order(t['updated_at'].desc)

    # z is a table alias
    z = i.as('recent_t')

    Serial.joins(
      Arel::Nodes::InnerJoin.new(z, Arel::Nodes::On.new(z['serial_id'].eq(p['id'])))
    ).pluck(:serial_id).uniq
  end

  def self.select_optimized(user_id, project_id)
    r = used_recently(user_id)
    h = {
      recent: (
        Serial.where('"serials"."id" IN (?)', r.first(10) ).order(:name).to_a +
        Serial.where(created_by_id: user_id, created_at: 3.hours.ago..Time.now).limit(5).to_a).uniq,
      pinboard: Serial.pinned_by(user_id).pinned_in_project(project_id).to_a
    }

    h[:quick] = (Serial.pinned_by(user_id).pinboard_inserted.pinned_in_project(project_id).to_a +
                 Serial.where('"serials"."id" IN (?)', r.first(4) ).order(:name).to_a).uniq
    h
  end

  protected

  # @return [Boolean]
  def sv_duplicate?
    if self.duplicate?
      soft_validations.add(:name, 'There is another serial with this name in the database.')
    end
    # TODO soft validation of name matching an alternate value for name of a different serial
  end

end