ophrescue/RescueRails

View on GitHub
app/models/cat.rb

Summary

Maintainability
B
4 hrs
Test Coverage
#    Copyright 2019 Operation Paws for Homes
#
#    Licensed under the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#    See the License for the specific language governing permissions and
#    limitations under the License.
#
# == Schema Information
#
# Table name: cats
#
#  id                      :bigint           not null, primary key
#  name                    :string
#  original_name           :string
#  tracking_id             :integer
#  primary_breed_id        :integer
#  secondary_breed_id      :integer
#  status                  :string
#  age                     :string(75)
#  size                    :string(75)
#  is_altered              :boolean
#  gender                  :string(6)
#  declawed                :boolean
#  litter_box_trained      :boolean
#  coat_length             :string
#  is_special_needs        :boolean
#  no_dogs                 :boolean
#  no_cats                 :boolean
#  no_kids                 :boolean
#  description             :text
#  foster_id               :integer
#  adoption_date           :date
#  is_uptodateonshots      :boolean          default(TRUE)
#  intake_dt               :date
#  available_on_dt         :date
#  has_medical_need        :boolean          default(FALSE)
#  is_high_priority        :boolean          default(FALSE)
#  needs_photos            :boolean          default(FALSE)
#  has_behavior_problem    :boolean          default(FALSE)
#  needs_foster            :boolean          default(FALSE)
#  petfinder_ad_url        :string
#  craigslist_ad_url       :string
#  youtube_video_url       :string
#  microchip               :string
#  fee                     :integer
#  coordinator_id          :integer
#  sponsored_by            :string
#  shelter_id              :integer
#  medical_summary         :text
#  behavior_summary        :text
#  medical_review_complete :boolean          default(FALSE)
#  first_shots             :string
#  second_shots            :string
#  third_shots             :string
#  rabies                  :string
#  felv_fiv_test           :string
#  flea_tick_preventative  :string
#  dewormer                :string
#  coccidia_treatment      :string
#  created_at              :datetime         not null
#  updated_at              :datetime         not null
#  hidden                  :boolean          default(FALSE), not null
#  no_urban_setting        :boolean          default(FALSE), not null
#  home_check_required     :boolean          default(FALSE), not null
#

class Cat < ApplicationRecord
  audited
  include Filterable
  include Flaggable
  include ClientValidated

  attr_accessor :primary_breed_name, :secondary_breed_name

  has_associated_audits

  belongs_to :primary_breed,   class_name: 'CatBreed', optional: true
  belongs_to :secondary_breed, class_name: 'CatBreed', optional: true
  belongs_to :foster,          class_name: 'User' ,    optional: true
  belongs_to :coordinator,     class_name: 'User',     optional: true
  belongs_to :shelter,                                 optional: true

  has_many :treatment_records, as: :treatable
  has_many :comments, -> { order 'created_at DESC' }, as: :commentable
  has_many :attachments, as: :attachable, dependent: :destroy
  has_many :photos, -> { order 'position ASC' }, dependent: :destroy, as: :animal
  has_many :cat_adoptions, dependent: :destroy
  has_many :adopters, through: :cat_adoptions

  accepts_nested_attributes_for :attachments, allow_destroy: true
  accepts_nested_attributes_for :photos, allow_destroy: true

  validates :name,
            presence: true,
            length: { maximum: 75 },
            uniqueness: { case_sensitive: false }

  validates :tracking_id, uniqueness: true, presence: true

  validates :microchip, uniqueness: true, allow_blank: true

  validate :microchip_check

  STATUSES = ['adoptable', 'adopted', 'adoption pending', 'trial adoption',
        'on hold', 'not available', 'return pending', 'coming soon', 'completed'].freeze
  validates_inclusion_of :status, in: STATUSES
  validates_presence_of :status

  # map standard validation messages onto attributes
  VALIDATION_ERROR_MESSAGES = {tracking_id: :numeric, name: :blank, status: :selected }.freeze

  PUBLIC_STATUSES = ['adoptable', 'adoption pending', 'coming soon'].freeze

  ACTIVE_STATUSES = [
    'adoptable',
    'adoption pending',
    'on hold',
    'return pending',
    'coming soon'
  ].freeze

  UNAVAILABLE_STATUSES = ['adopted', 'completed', 'not available'].freeze

  PETFINDER_STATUS = {
    'adoptable' => 'A',
    'adoption pending' => 'P',
    'on hold' => 'H',
    'return pending' => 'H',
    'coming soon' => 'A'
  }.freeze

  PETFINDER_SIZE = {
    'small' => 'S',
    'medium' => 'M',
    'large' => 'L',
    'extra large' => 'XL'
  }.freeze

  PETFINDER_GENDER = {
    'Male' => 'M',
    'Female' => 'F'
  }.freeze

  AGES = %w[baby young adult senior].freeze
  validates_inclusion_of :age, in: AGES, allow_blank: true

  SIZES = ['small', 'medium', 'large', 'extra large'].freeze
  validates_inclusion_of :size, in: SIZES, allow_blank: true

  GENDERS = %w[Male Female].freeze
  validates_inclusion_of :gender, in: GENDERS, allow_blank: true

  COAT_LENGTH = ['hairless','short','medium','long','wire','curly'].freeze
  validates_inclusion_of :coat_length, in: COAT_LENGTH, allow_blank: true

  SEARCH_FIELDS = ["Cat Breed", "Tracking ID", "Name", "Microchip"].freeze

  before_save :update_adoption_date, :update_needs_foster

  scope :is_breed,                                ->(breed_partial) { joins("join cat_breeds on (cat_breeds.id = cats.primary_breed_id) or (cat_breeds.id = cats.secondary_breed_id)").where("cat_breeds.name ilike '%#{sanitize_sql_like(breed_partial)}%'").distinct }
  scope :pattern_matching_name,                   ->(search_term) { where("name ilike ?", search_term) }
  scope :autocomplete_filter,                     -> { where(status: Cat::ACTIVE_STATUSES)}

  # Rails 5.2 issues deprecation errors for any order that is not column names
  # so arel is the workaround
  scope :sort_with_search_term_matches_first,     ->(search_term) { order(Cat.arel_table[:name].does_not_match("#{search_term}%"), "tracking_id asc") }
  scope :gallery_view,                            -> { includes(:primary_breed, :secondary_breed, :photos, :foster).where(status: Cat::PUBLIC_STATUSES).where('hidden' => false).status_order.order(:tracking_id) }

  def self.autocomplete_name(search_term = nil)
    if search_term.present?
      select(:name, :id)
        .pattern_matching_name("%"+search_term+"%")
        .autocomplete_filter
        .sort_with_search_term_matches_first(search_term)
    else
      select(:name, :id)
    end
  end

  def microchip_check
    if !self.microchip.nil?
      case self.microchip.length
      when 0
        return
      when 10
        valid_format = /\A[a-zA-Z0-9]{10}\z/
        err_message = ": 10 digit format -> This format accepts only numbers and characters"
      when 15
        valid_format = /\A9[0-9]{14}\z/
        err_message = ": 15 digits format -> This format needs to start with 9, and accepts only numbers"
      else
        return errors.add(:microchip, "length must be 10 or 15")
      end
      if !valid_format.match?(self.microchip)
        return errors.add(:microchip, err_message)
      end
    end
  end

  def self.search(search_params)
    search_term, search_field = search_params
    return unscoped if invalid_search_params(search_params)
    return is_breed(search_term) if search_field == "cat_breed"
    return where("#{search_field} = ?", "#{search_term.strip}") if search_field == "tracking_id"
    return where("#{search_field} ilike ?", "%#{search_term.strip}%")
  end

  def self.invalid_search_params(search_params)
    search_term, search_field = search_params
    # security check, since search field will be injected into SQL query
    (search_params.compact.length != 2) || (!%w[name tracking_id microchip cat_breed].include? search_field)
  end

  def name=(name)
    write_attribute(:name, name.strip)
  end

  def breeds
    [ (primary_breed&.name), (secondary_breed&.name) ].compact
  end

  def primary_photo_url
    if Rails.env.development?
      # helps with formulating the css, and UI design, on the CatsController#index page
      # shouldn't be used longterm, as it uses actual urls, which will expire in time.
      # it would be good to have a longterm solution that has actual photos
      AWS_PHOTO_URLS.sample()
    else
      photos.visible.empty? ?
        Photo.no_photo_url :
        photos.visible.first.photo.url(:medium)
    end
  end

  def primary_breed_name
    primary_breed&.name
  end

  def secondary_breed_name
    secondary_breed&.name
  end

  def photo_alt_text
    primary_breed ?  primary_breed.name : name
  end

  def foster_location
    foster && foster.location
  end

  def adopted?
    status == 'adopted'
  end

  def status_key
    status.gsub(/\s/,"_")
  end

  def unavailable?
    UNAVAILABLE_STATUSES.include?(status)
  end

  def is_accepting_applications?
    PUBLIC_STATUSES.include?(status)
  end

  def attributes_to_audit
    %w[status]
  end

  def audits_and_associated_audits
    (audits + associated_audits).sort_by(&:created_at).reverse!
  end

  def comments_and_audits_and_associated_audits
    (persisted_comments + audits + associated_audits).sort_by(&:created_at).reverse!
  end

  def self.status_order
    order(Arel.sql("
      CASE
        WHEN status = 'adoptable' THEN '1'
        WHEN status = 'coming soon' THEN '2'
        WHEN status = 'adoption pending' THEN '3'
      END
      "))
  end

  def to_petfinder_status
    PETFINDER_STATUS[status]
  end

  def to_petfinder_size
    PETFINDER_SIZE[size]
  end

  def to_petfinder_gender
    PETFINDER_GENDER[gender]
  end

  def update_adoption_date
    return unless status_changed?
    return if status == 'completed'

    self.adoption_date = nil
    self.adoption_date = Date.today() if adopted?
  end

  def update_needs_foster
    return unless status_changed?
    return unless ['completed', 'adopted', 'trial adoption'].include?(status)

    self.needs_foster = false
  end

  def self.next_tracking_id
    connection.select_value("SELECT nextval('cat_tracking_id_seq')")
  end

  def persisted_comments
    comments.select(&:persisted?)
  end
end

CatsManager = Cat