mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-10-31 11:21:06 -04:00
222 lines
8.1 KiB
Ruby
222 lines
8.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Garden < ApplicationRecord
|
|
extend FriendlyId
|
|
include Geocodable
|
|
include PhotoCapable
|
|
include Ownable
|
|
|
|
friendly_id :garden_slug, use: %i(slugged finders)
|
|
|
|
has_many :plantings, dependent: :destroy
|
|
has_many :crops, through: :plantings
|
|
has_many :jobs, dependent: :destroy
|
|
has_many :planting_histories, dependent: :destroy
|
|
has_many :garden_collaborators, dependent: :destroy
|
|
|
|
belongs_to :garden_type, optional: true
|
|
|
|
# set up geocoding
|
|
geocoded_by :location
|
|
before_validation :strip_blanks
|
|
after_validation :cleanup_area
|
|
after_validation :geocode, if: :will_save_change_to_location?
|
|
after_validation :empty_unwanted_geocodes, if: :will_save_change_to_location?
|
|
after_validation :populate_wikidata_info, if: :will_save_change_to_location?
|
|
after_save :mark_inactive_garden_plantings_as_finished
|
|
|
|
scope :active, -> { where(active: true) }
|
|
scope :inactive, -> { where(active: false) }
|
|
scope :nurseries, -> { where(nursery: true) }
|
|
scope :not_nurseries, -> { where(nursery: false) }
|
|
scope :order_by_name, -> { order(Arel.sql("lower(name) asc")) }
|
|
|
|
validates :location, length: { maximum: 255 }
|
|
validates :slug, uniqueness: true
|
|
validates :nursery, uniqueness: { scope: :owner_id, message: :one_nursery_per_member }, if: :nursery?
|
|
|
|
validates :name, uniqueness: { scope: :owner_id }
|
|
validates :name,
|
|
format: { without: /\n/, message: :no_newlines },
|
|
allow_blank: false, presence: true,
|
|
length: { maximum: 255 }
|
|
|
|
validates :area,
|
|
numericality: { only_integer: false, greater_than_or_equal_to: 0 },
|
|
allow_nil: true
|
|
|
|
# The layout grid. Capped so a bed's map stays a sensible number of cells to
|
|
# draw, and so shrinking it can't quietly strand a planting off the edge.
|
|
MAX_GRID_SIZE = 50
|
|
validates :grid_columns, :grid_rows,
|
|
numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_GRID_SIZE }
|
|
validate :grid_must_not_cut_off_plantings
|
|
|
|
# The things on the layout that aren't plants. Each is a hash with a kind and a
|
|
# position in grid cells (x, y). A line (a row, path, drip line, fence or
|
|
# trellis) runs from there to (x2, y2), and an area (netting) is the rectangle
|
|
# with those two corners. A label has text, and lines and areas can.
|
|
LAYOUT_POINT_KINDS = %w(stone sprinkler tap stake label).freeze
|
|
LAYOUT_SPAN_KINDS = %w(row path dripline fence trellis netting).freeze
|
|
LAYOUT_FEATURE_KINDS = (LAYOUT_POINT_KINDS + LAYOUT_SPAN_KINDS).freeze
|
|
MAX_LAYOUT_FEATURES = 200
|
|
MAX_LAYOUT_TEXT = 40
|
|
validate :layout_features_must_fit
|
|
|
|
scope :located, lambda {
|
|
where.not(gardens: { location: '' })
|
|
.where.not(gardens: { latitude: nil })
|
|
.where.not(gardens: { longitude: nil })
|
|
}
|
|
|
|
AREA_UNITS_VALUES = {
|
|
"square metres" => "square metre",
|
|
"square feet" => "square foot",
|
|
"hectares" => "hectare",
|
|
"acres" => "acre"
|
|
}.freeze
|
|
validates :area_unit, inclusion: { in: AREA_UNITS_VALUES.values,
|
|
message: :not_a_valid_area_unit },
|
|
allow_blank: true
|
|
|
|
def cleanup_area
|
|
self.area = nil if area&.zero?
|
|
self.area_unit = nil if area.blank?
|
|
end
|
|
|
|
def garden_slug
|
|
"#{owner.login_name}-#{name}".downcase.tr(' ', '-')
|
|
end
|
|
|
|
def to_s
|
|
name
|
|
end
|
|
|
|
# When you mark a garden as inactive, all the plantings in it should be
|
|
# marked as finished. This automates that.
|
|
def mark_inactive_garden_plantings_as_finished
|
|
return unless active == false
|
|
|
|
plantings.current.each do |p|
|
|
p.finished = true
|
|
p.save
|
|
end
|
|
end
|
|
|
|
# Makes this the owner's nursery. They can only have one, so any garden they
|
|
# had marked as the nursery before goes back to being an ordinary garden.
|
|
def make_nursery!
|
|
transaction do
|
|
self.class.where(owner_id:, nursery: true).where.not(id:).update_all(nursery: false) # rubocop:disable Rails/SkipsModelValidations
|
|
update!(nursery: true)
|
|
owner.accept_nursery!
|
|
end
|
|
end
|
|
|
|
# Deactivate any gardens with no active plantings
|
|
def self.archive!(time_limit: 3.years.ago, limit: 1000)
|
|
Garden.active.not_nurseries.where("gardens.updated_at < ?", time_limit).order(updated_at: :asc).limit(limit).each do |active_garden|
|
|
unless active_garden.plantings.active.any?
|
|
active_garden.active = false
|
|
active_garden.save
|
|
end
|
|
end
|
|
end
|
|
|
|
def populate_wikidata_info
|
|
return false if location.blank?
|
|
|
|
wd_id = WikidataService.find_wikidata_id(location)
|
|
return false if wd_id.blank?
|
|
|
|
self.location_wikidata_id = wd_id
|
|
temps = WikidataService.fetch_temps(wd_id)
|
|
self.highest_temp_c = temps[:highest_temp_c]
|
|
self.lowest_temp_c = temps[:lowest_temp_c]
|
|
true
|
|
end
|
|
|
|
# How many cells the bed's layout grid has, for callers that just want the size.
|
|
def grid_cells
|
|
grid_columns * grid_rows
|
|
end
|
|
|
|
def has_layout?
|
|
layout_saved_at.present? || placed_plants.any? || layout_features.present?
|
|
end
|
|
|
|
# What's wrong with a list of layout features, if anything, as messages. Used
|
|
# by the layout's save as well as by validation, so a drag can be checked
|
|
# without a full save (which would geocode the garden every time).
|
|
def layout_feature_problems(features, columns: grid_columns, rows: grid_rows)
|
|
return ['The garden features must be a list'] unless features.is_a?(Array)
|
|
return ["A bed can have at most #{MAX_LAYOUT_FEATURES} garden features"] if features.size > MAX_LAYOUT_FEATURES
|
|
|
|
features.filter_map { |feature| layout_feature_problem(feature, columns, rows) }.uniq
|
|
end
|
|
|
|
# The plants currently drawn on this bed's layout.
|
|
def placed_plants
|
|
Plant.placed.where(planting_id: plantings.current.select(:id))
|
|
end
|
|
|
|
# Every plant of every planting growing here, placed or not. What the layout
|
|
# page may rearrange.
|
|
def placed_or_owned_plants
|
|
Plant.where(planting_id: plantings.current.select(:id))
|
|
end
|
|
|
|
# Gives each planting growing here its plants, the first time the layout is
|
|
# used, rather than making them for every planting whether or not it ever
|
|
# goes on a layout. Plantings that already have plants are left alone, and
|
|
# so is one composted down to none. All in one insert.
|
|
def prepare_layout
|
|
now = Time.current
|
|
rows = plantings.current.where.not(id: Plant.select(:planting_id)).flat_map do |planting|
|
|
Array.new(planting.plant_count) { { planting_id: planting.id, created_at: now, updated_at: now } }
|
|
end
|
|
Plant.insert_all(rows) if rows.any? # rubocop:disable Rails/SkipsModelValidations
|
|
end
|
|
|
|
protected
|
|
|
|
def strip_blanks
|
|
self.name = name.strip unless name.nil?
|
|
end
|
|
|
|
# Shrinking the grid must not leave a placed plant hanging over the edge,
|
|
# which would otherwise only show up as a plant missing from the map.
|
|
def grid_must_not_cut_off_plantings
|
|
return if new_record? || grid_columns.blank? || grid_rows.blank?
|
|
return unless will_save_change_to_grid_columns? || will_save_change_to_grid_rows?
|
|
return if placed_plants.outside_grid(grid_columns, grid_rows).none? &&
|
|
layout_feature_problems(layout_features).empty?
|
|
|
|
errors.add(:base, :grid_too_small_for_plantings)
|
|
end
|
|
|
|
def layout_features_must_fit
|
|
return unless will_save_change_to_layout_features?
|
|
|
|
layout_feature_problems(layout_features).each { |problem| errors.add(:base, problem) }
|
|
end
|
|
|
|
def layout_feature_problem(feature, columns, rows)
|
|
known = feature.is_a?(Hash) && LAYOUT_FEATURE_KINDS.include?(feature['kind'])
|
|
return "That isn't something that can go on the bed" unless known
|
|
|
|
ends = [%w(x y)]
|
|
ends << %w(x2 y2) if LAYOUT_SPAN_KINDS.include?(feature['kind'])
|
|
return 'A garden feature is off the bed' unless ends.all? { |col, row| on_bed?(feature[col], feature[row], columns, rows) }
|
|
|
|
text = feature['text'].to_s
|
|
return 'A label needs something written on it' if feature['kind'] == 'label' && text.strip.empty?
|
|
|
|
"A label or row name can be at most #{MAX_LAYOUT_TEXT} characters" if text.length > MAX_LAYOUT_TEXT
|
|
end
|
|
|
|
def on_bed?(col, row, columns, rows)
|
|
col.is_a?(Numeric) && row.is_a?(Numeric) && col.between?(0, columns) && row.between?(0, rows)
|
|
end
|
|
end
|