ujh/fountainpencompanion

View on GitHub
app/models/collected_ink.rb

Summary

Maintainability
B
6 hrs
Test Coverage
require "csv"

class CollectedInk < ApplicationRecord
  include Archivable
  include PgSearch::Model

  KINDS = %w[bottle sample cartridge swab]

  validates :kind, inclusion: { in: KINDS, allow_blank: true }
  validates :brand_name, length: { in: 1..100 }
  validates :ink_name, length: { in: 1..100 }
  validates :line_name, length: { in: 1..100, allow_blank: true }

  validate :color_valid

  before_save :simplify
  before_save :add_comment

  belongs_to :user
  has_many :currently_inkeds, dependent: :destroy
  has_many :usage_records, through: :currently_inkeds
  has_one :newest_currently_inked,
          -> { order("inked_on desc") },
          class_name: "CurrentlyInked"

  belongs_to :micro_cluster, optional: true

  pg_search_scope(
    :search,
    against: %i[brand_name line_name ink_name],
    using: {
      tsearch: {
        dictionary: "english",
        tsvector_column: "tsv"
      }
    }
  )

  pg_search_scope(
    :kinda_similar_search,
    against: %i[brand_name line_name ink_name],
    using: {
      tsearch: {
        dictionary: "english",
        tsvector_column: "tsv"
      },
      trigram: {
      }
    }
  )

  Gutentag::ActiveRecord.call self

  delegate :macro_cluster, to: :micro_cluster, allow_nil: true

  def cluster_tags
    return [] unless macro_cluster

    macro_cluster.tags
  end

  def cluster_description
    return "" unless macro_cluster

    macro_cluster.description
  end

  def brand_description
    return "" unless macro_cluster
    return "" unless macro_cluster.brand_cluster

    macro_cluster.brand_cluster.description
  end

  def tags_as_string
    tag_names.join(", ")
  end

  def tags_as_string=(string)
    self.tag_names = string.split(",").map(&:strip)
  end

  def si
    [simplified_brand_name, simplified_line_name, simplified_ink_name]
  end

  def self.without_color
    where(color: "")
  end

  def self.with_color
    where.not(color: "")
  end

  def self.alphabetical
    order("brand_name, line_name, ink_name")
  end

  def self.brand_count
    reorder(:simplified_brand_name)
      .group(:simplified_brand_name)
      .pluck(:simplified_brand_name)
      .size
  end

  def self.unique_inks_per_brand(name)
    # Ignore the simplified_line_name here as it's unlikely that a single brand will have the same
    # ink name in two different lines.
    where(simplified_brand_name: name).group(:simplified_ink_name).count.size
  end

  def self.brands
    reorder(:brand_name).group(:brand_name).pluck(:brand_name)
  end

  def self.bottles
    where(kind: "bottle")
  end

  def self.bottle_count
    bottles.count
  end

  def self.samples
    where(kind: "sample")
  end

  def self.sample_count
    samples.count
  end

  def self.cartridges
    where(kind: "cartridge")
  end

  def self.unswabbed_count
    where(swabbed: false).count
  end

  def self.cartridge_count
    cartridges.count
  end

  def self.to_csv
    CSV.generate(col_sep: ";") do |csv|
      csv << [
        "Brand",
        "Line",
        "Name",
        "Type",
        "Color",
        "Swabbed",
        "Used",
        "Comment",
        "Private Comment",
        "Archived",
        "Usage",
        "Tags",
        "Date Added"
      ]
      all.each do |ci|
        csv << [
          ci.brand_name,
          ci.line_name,
          ci.ink_name,
          ci.kind,
          ci.color,
          ci.swabbed,
          ci.used,
          ci.comment,
          ci.private_comment,
          ci.archived?,
          ci.currently_inkeds.length,
          ci.tags_as_string,
          ci.created_at.to_date.to_s
        ]
      end
    end
  end

  def color
    read_attribute(:color).presence || cluster_color
  end

  def color=(value)
    super(value.strip) if value.strip != cluster_color
  end

  def name
    n = short_name
    n = "#{n} - #{kind}" if kind.present?
    n = "#{n} (archived)" if archived?
    n
  end

  def short_name
    [brand_name, line_name, ink_name].reject { |f| f.blank? }.join(" ")
  end

  def brand_name=(value)
    super(value.strip)
  end

  def line_name=(value)
    super(value.strip)
  end

  def ink_name=(value)
    super(value.strip)
  end

  def simplified_name
    "#{simplified_brand_name}#{simplified_ink_name}"
  end

  def last_used_on
    newest_currently_inked&.last_used_on || newest_currently_inked&.inked_on
  end

  def usage_count
    currently_inkeds.size
  end

  def daily_usage_count
    usage_records.size
  end

  private

  def add_comment
    unless changed.any? { |c| %w[brand_name line_name ink_name].include?(c) }
      return
    end
    return unless comment.blank?

    rel =
      self
        .user
        .collected_inks
        .where(
          "LOWER(brand_name) = ? AND LOWER(line_name) = ? AND LOWER(ink_name) = ?",
          brand_name.to_s.downcase,
          line_name.to_s.downcase,
          ink_name.to_s.downcase
        )
        .where(kind: kind)
    rel = rel.where("id <> ?", id) if persisted?

    if rel.exists?
      self.comment = [
        kind.capitalize.presence,
        "no.",
        rel.count + 1
      ].compact.join(" ")
    end
  end

  def simplify
    Simplifier.for_collected_ink(self).run
  end

  def color_valid
    return if read_attribute(:color).blank?
    if read_attribute(:color) !~ /#[0-9a-f]{3}([0-9a-f][3])?/i
      errors.add(
        :color,
        "Only valid HTML color codes are supported (e.g #fff or #efefef)"
      )
      return
    end

    Color::RGB.from_html(read_attribute(:color))
  rescue ArgumentError
    errors.add(
      :color,
      "Only valid HTML color codes are supported (e.g #fff or #efefef)"
    )
  end
end