<commit_before>class Crop < ApplicationRecord
  extend FriendlyId
  include PhotoCapable
  friendly_id :name, use: %i(slugged finders)

  ##
  ## Triggers
  before_destroy { |crop| crop.posts.clear }

  ##
  ## Relationships
  has_many :scientific_names, dependent: :destroy
  accepts_nested_attributes_for :scientific_names, allow_destroy: true, reject_if: :all_blank
  has_many :alternate_names, dependent: :destroy
  has_many :plantings, dependent: :destroy
  has_many :seeds, dependent: :destroy
  has_many :harvests, dependent: :destroy
  has_many :photo_associations, dependent: :destroy
  has_many :photos, through: :photo_associations
  has_many :plant_parts, -> { joins_members.distinct.order("plant_parts.name") }, through: :harvests
  belongs_to :creator, class_name: 'Member', optional: true, inverse_of: :created_crops
  belongs_to :requester, class_name: 'Member', optional: true, inverse_of: :requested_crops
  belongs_to :parent, class_name: 'Crop', optional: true, inverse_of: :varieties
  has_many :varieties, class_name: 'Crop', foreign_key: 'parent_id', dependent: :nullify, inverse_of: :parent
  has_and_belongs_to_many :posts # rubocop:disable Rails/HasAndBelongsToMany

  ##
  ## Scopes
  scope :recent, -> { approved.order(created_at: :desc) }
  scope :toplevel, -> { approved.where(parent_id: nil) }
  scope :popular, -> { approved.order(Arel.sql("plantings_count desc, lower(name) asc")) }
  scope :pending_approval, -> { where(approval_status: "pending") }
  scope :approved, -> { where(approval_status: "approved") }
  scope :rejected, -> { where(approval_status: "rejected") }
  scope :interesting, -> { approved.has_photos }
  scope :has_photos, -> { includes(:photos).where.not(photos: { id: nil }) }
  scope :joins_members, -> { joins("INNER JOIN members ON members.id = harvests.owner_id") }

  # Special scope to control if it's in the search index
  scope :search_import, -> { approved }

  ##
  ## Validations
  # Reasons are only necessary when rejecting
  validates :reason_for_rejection, presence: true, if: :rejected?
  ## This validation addresses a race condition
  validate :approval_status_cannot_be_changed_again
  validate :must_be_rejected_if_rejected_reasons_present
  validate :must_have_meaningful_reason_for_rejection
  ## Wikipedia urls are only necessary when approving a crop
  validates :en_wikipedia_url,
            format: {
              with:    %r{\Ahttps?:\/\/en\.wikipedia\.org\/wiki\/[[:alnum:]%_\.()-]+\z},
              message: 'is not a valid English Wikipedia URL'
            },
            if:     :approved?

  ####################################
  # Elastic search configuration
  searchkick word_start: %i(name alternate_names scientific_names), case_sensitive: false if ENV["GROWSTUFF_ELASTICSEARCH"] == "true"

  def to_s
    name
  end

  def to_param
    slug
  end

  def default_scientific_name
    scientific_names.first&.name
  end

  # returns hash indicating whether this crop is grown in
  # sun/semi-shade/shade
  # key: sunniness (eg. 'sun')
  # value: count of how many times it's been used by plantings
  def sunniness
    count_uses_of_property 'sunniness'
  end

  # returns a hash of propagation methods (seed, seedling, etc),
  # key: propagation method (eg. 'seed')
  # value: count of how many times it's been used by plantings
  def planted_from
    count_uses_of_property 'planted_from'
  end

  # returns a hash of most harvested plant parts (fruit, seed, etc)
  # key: plant part (eg. 'fruit')
  # value: count of how many times it's been used by harvests
  def popular_plant_parts
    PlantPart.joins(:harvests)
      .where("crop_id = ?", id)
      .order("count_harvests_id DESC")
      .group("plant_parts.id", "plant_parts.name")
      .count("harvests.id")
  end

  def annual?
    perennial == false
  end

  def interesting?
    photos.size >= 3 || plantings_count >= 3
  end

  def pending?
    approval_status == "pending"
  end

  def approved?
    approval_status == "approved"
  end

  def rejected?
    approval_status == "rejected"
  end

  def approval_statuses
    %w(rejected pending approved)
  end

  def reasons_for_rejection
    ["already in database", "not edible", "not enough information", "other"]
  end

  def rejection_explanation
    return rejection_notes if reason_for_rejection == "other"

    reason_for_rejection
  end

  def update_medians
    plantings.each(&:update_harvest_days!)
    update_lifespan_medians
    update_harvest_medians
  end

  def update_lifespan_medians
    # Median lifespan of plantings
    update(median_lifespan: Planting.where(crop: self).median(:lifespan))
  end

  def update_harvest_medians
    update(median_days_to_first_harvest: Planting.where(crop: self).median(:days_to_first_harvest))
    update(median_days_to_last_harvest: Planting.where(crop: self).median(:days_to_last_harvest))
  end

  def self.case_insensitive_name(name)
    where(["lower(crops.name) = :value", { value: name.downcase }])
  end

  def should_index?
    approved?
  end

  def search_data
    {
      name:             name,
      alternate_names:  alternate_names.pluck(:name),
      scientific_names: scientific_names.pluck(:name),
      plantings_count:  plantings_count, # boost the crops that are planted the most
      planters_ids:     plantings.pluck(:owner_id) # boost this product for these members
    }
  end

  private

  def count_uses_of_property(col_name)
    plantings.unscoped
      .where(crop_id: id)
      .where.not(col_name => nil)
      .group(col_name)
      .count
  end

  # Custom validations
  def approval_status_cannot_be_changed_again
    previous = previous_changes.include?(:approval_status) ? previous_changes.approval_status : {}
    return unless previous.include?(:rejected) || previous.include?(:approved)

    errors.add(:approval_status, "has already been set to #{approval_status}")
  end

  def must_be_rejected_if_rejected_reasons_present
    return if rejected?
    return unless reason_for_rejection.present? || rejection_notes.present?

    errors.add(:approval_status, "must be rejected if a reason for rejection is present")
  end

  def must_have_meaningful_reason_for_rejection
    return unless reason_for_rejection == "other" && rejection_notes.blank?

    errors.add(:rejection_notes, "must be added if the reason for rejection is \"other\"")
  end
end
<commit_msg>If a crop is deleted, delete association (gerund) records too<commit_after>class Crop < ApplicationRecord
  extend FriendlyId
  include PhotoCapable
  friendly_id :name, use: %i(slugged finders)

  ##
  ## Triggers
  before_destroy { |crop| crop.posts.clear }

  ##
  ## Relationships
  has_many :scientific_names, dependent: :delete_all
  accepts_nested_attributes_for :scientific_names, allow_destroy: true, reject_if: :all_blank
  has_many :alternate_names, dependent: :delete_all
  has_many :plantings, dependent: :destroy
  has_many :seeds, dependent: :destroy
  has_many :harvests, dependent: :destroy
  has_many :photo_associations, dependent: :delete_all
  has_many :photos, through: :photo_associations
  has_many :plant_parts, -> { joins_members.distinct.order("plant_parts.name") }, through: :harvests
  belongs_to :creator, class_name: 'Member', optional: true, inverse_of: :created_crops
  belongs_to :requester, class_name: 'Member', optional: true, inverse_of: :requested_crops
  belongs_to :parent, class_name: 'Crop', optional: true, inverse_of: :varieties
  has_many :varieties, class_name: 'Crop', foreign_key: 'parent_id', dependent: :nullify, inverse_of: :parent
  has_and_belongs_to_many :posts # rubocop:disable Rails/HasAndBelongsToMany

  ##
  ## Scopes
  scope :recent, -> { approved.order(created_at: :desc) }
  scope :toplevel, -> { approved.where(parent_id: nil) }
  scope :popular, -> { approved.order(Arel.sql("plantings_count desc, lower(name) asc")) }
  scope :pending_approval, -> { where(approval_status: "pending") }
  scope :approved, -> { where(approval_status: "approved") }
  scope :rejected, -> { where(approval_status: "rejected") }
  scope :interesting, -> { approved.has_photos }
  scope :has_photos, -> { includes(:photos).where.not(photos: { id: nil }) }
  scope :joins_members, -> { joins("INNER JOIN members ON members.id = harvests.owner_id") }

  # Special scope to control if it's in the search index
  scope :search_import, -> { approved }

  ##
  ## Validations
  # Reasons are only necessary when rejecting
  validates :reason_for_rejection, presence: true, if: :rejected?
  ## This validation addresses a race condition
  validate :approval_status_cannot_be_changed_again
  validate :must_be_rejected_if_rejected_reasons_present
  validate :must_have_meaningful_reason_for_rejection
  ## Wikipedia urls are only necessary when approving a crop
  validates :en_wikipedia_url,
            format: {
              with:    %r{\Ahttps?:\/\/en\.wikipedia\.org\/wiki\/[[:alnum:]%_\.()-]+\z},
              message: 'is not a valid English Wikipedia URL'
            },
            if:     :approved?

  ####################################
  # Elastic search configuration
  searchkick word_start: %i(name alternate_names scientific_names), case_sensitive: false if ENV["GROWSTUFF_ELASTICSEARCH"] == "true"

  def to_s
    name
  end

  def to_param
    slug
  end

  def default_scientific_name
    scientific_names.first&.name
  end

  # returns hash indicating whether this crop is grown in
  # sun/semi-shade/shade
  # key: sunniness (eg. 'sun')
  # value: count of how many times it's been used by plantings
  def sunniness
    count_uses_of_property 'sunniness'
  end

  # returns a hash of propagation methods (seed, seedling, etc),
  # key: propagation method (eg. 'seed')
  # value: count of how many times it's been used by plantings
  def planted_from
    count_uses_of_property 'planted_from'
  end

  # returns a hash of most harvested plant parts (fruit, seed, etc)
  # key: plant part (eg. 'fruit')
  # value: count of how many times it's been used by harvests
  def popular_plant_parts
    PlantPart.joins(:harvests)
      .where("crop_id = ?", id)
      .order("count_harvests_id DESC")
      .group("plant_parts.id", "plant_parts.name")
      .count("harvests.id")
  end

  def annual?
    perennial == false
  end

  def interesting?
    photos.size >= 3 || plantings_count >= 3
  end

  def pending?
    approval_status == "pending"
  end

  def approved?
    approval_status == "approved"
  end

  def rejected?
    approval_status == "rejected"
  end

  def approval_statuses
    %w(rejected pending approved)
  end

  def reasons_for_rejection
    ["already in database", "not edible", "not enough information", "other"]
  end

  def rejection_explanation
    return rejection_notes if reason_for_rejection == "other"

    reason_for_rejection
  end

  def update_medians
    plantings.each(&:update_harvest_days!)
    update_lifespan_medians
    update_harvest_medians
  end

  def update_lifespan_medians
    # Median lifespan of plantings
    update(median_lifespan: Planting.where(crop: self).median(:lifespan))
  end

  def update_harvest_medians
    update(median_days_to_first_harvest: Planting.where(crop: self).median(:days_to_first_harvest))
    update(median_days_to_last_harvest: Planting.where(crop: self).median(:days_to_last_harvest))
  end

  def self.case_insensitive_name(name)
    where(["lower(crops.name) = :value", { value: name.downcase }])
  end

  def should_index?
    approved?
  end

  def search_data
    {
      name:             name,
      alternate_names:  alternate_names.pluck(:name),
      scientific_names: scientific_names.pluck(:name),
      plantings_count:  plantings_count, # boost the crops that are planted the most
      planters_ids:     plantings.pluck(:owner_id) # boost this product for these members
    }
  end

  private

  def count_uses_of_property(col_name)
    plantings.unscoped
      .where(crop_id: id)
      .where.not(col_name => nil)
      .group(col_name)
      .count
  end

  # Custom validations
  def approval_status_cannot_be_changed_again
    previous = previous_changes.include?(:approval_status) ? previous_changes.approval_status : {}
    return unless previous.include?(:rejected) || previous.include?(:approved)

    errors.add(:approval_status, "has already been set to #{approval_status}")
  end

  def must_be_rejected_if_rejected_reasons_present
    return if rejected?
    return unless reason_for_rejection.present? || rejection_notes.present?

    errors.add(:approval_status, "must be rejected if a reason for rejection is present")
  end

  def must_have_meaningful_reason_for_rejection
    return unless reason_for_rejection == "other" && rejection_notes.blank?

    errors.add(:rejection_notes, "must be added if the reason for rejection is \"other\"")
  end
end

{'fraction_non_alphanumeric': 0.10365328952515059, 'fraction_numerical': 0.00031291559101932255, 'mean_word_length': 4.077045274027006, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 31, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2595529', 'n_tokens_mistral': 4232, 'n_tokens_neox': 4053, 'n_words': 1029}