mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-10-29 10:18:29 -04:00
308 lines
12 KiB
Ruby
308 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class GardensController < DataController
|
|
# Anyone can look at a member's inactive gardens, as they can their current ones.
|
|
# A garden's layout is public in the same way its show page is.
|
|
skip_before_action :authenticate_member!, only: %i(inactive layout)
|
|
skip_load_and_authorize_resource only: :inactive
|
|
|
|
def index
|
|
@owner = Member.find_by!(slug: params[:member_slug]) if params[:member_slug].present?
|
|
|
|
@gardens = @gardens.includes(:owner)
|
|
@gardens = @gardens.active
|
|
@own_gardens = @owner.present? && @owner == current_member
|
|
if @owner.present?
|
|
@nursery = @owner.nursery
|
|
@gardens = @gardens.not_nurseries # shown at the top instead
|
|
@gardens = @gardens.left_joins(:garden_collaborators)
|
|
@gardens = @gardens.where(owner: @owner).or(@gardens.where(garden_collaborators: { member: @owner }))
|
|
end
|
|
@gardens = @gardens.where.not(members: { confirmed_at: nil })
|
|
.order(:name).paginate(page: params[:page])
|
|
respond_with(@gardens)
|
|
end
|
|
|
|
# A member's gardens that are no longer active, as a gallery of what they grew.
|
|
def inactive
|
|
@owner = Member.confirmed.find_by!(slug: params[:member_slug])
|
|
owned = Garden.inactive.where(owner: @owner)
|
|
collaborating = Garden.inactive.where(id: GardenCollaborator.where(member: @owner).select(:garden_id))
|
|
@gardens = owned.or(collaborating)
|
|
.includes(:owner, plantings: { crop: { parent: :parent } })
|
|
.order(updated_at: :desc)
|
|
.paginate(page: params[:page], per_page: 12)
|
|
end
|
|
|
|
def show
|
|
@current_plantings = @garden.plantings.current.where.not(failed: true).includes(:crop, :owner).order(planted_at: :desc)
|
|
load_jobs
|
|
@finished_plantings = @garden.plantings.finished.includes(:crop)
|
|
@suggested_companions = Crop.approved.where(
|
|
id: CropCompanion.where(crop_a_id: @current_plantings.select(:crop_id)).select(:crop_b_id)
|
|
).order(:name)
|
|
@photos = @garden.photos.includes(:owner).order(created_at: :desc, id: :desc).paginate(page: params[:page])
|
|
@suggestion_count = MemberDashboard.new(current_member).easy_to_sow(40).size if @garden.active && @garden.owner == current_member
|
|
respond_with(@garden)
|
|
end
|
|
|
|
# A map of where things are planted in the bed. Anyone who can see the garden
|
|
# can see its layout; only the owner and collaborators can rearrange it.
|
|
def layout
|
|
@owner = @garden.owner
|
|
@editable = can?(:update_layout, @garden)
|
|
# Someone who can arrange the bed is here to use the layout, so this is when
|
|
# its plantings get their plants. Just looking doesn't make any.
|
|
@garden.prepare_layout if @editable
|
|
@layout = GardenLayoutSerializer.new(@garden, editable: @editable, resizable: can?(:update, @garden)).as_json
|
|
# The island reloads this after planting something new, since the planting
|
|
# form answers with a garden card rather than a layout.
|
|
render json: @layout if request.format.json?
|
|
end
|
|
|
|
# Saves the whole arrangement in one go, rather than a request per drag.
|
|
#
|
|
# Every plant is taken off the grid before the new positions are applied, so
|
|
# any plant the request leaves out ends up off the bed. A single bad placement
|
|
# rolls the whole thing back, leaving the bed as it was rather than half
|
|
# rearranged.
|
|
#
|
|
# A planting behaves like a stack: dragging one off it puts a plant on the
|
|
# bed, and the planting's quantity follows from how many plants it ends up
|
|
# with rather than being typed in. Composting a plant is the way back down.
|
|
def update_layout
|
|
@layout_errors = {}
|
|
placements = layout_params
|
|
composted = compost_params
|
|
features = features_params
|
|
saved = Garden.transaction do
|
|
# In case something was planted since the page was opened.
|
|
@garden.prepare_layout
|
|
compost(composted)
|
|
clear_positions
|
|
apply_placements(placements)
|
|
apply_features(features) unless features.nil?
|
|
raise ActiveRecord::Rollback if @layout_errors.present?
|
|
|
|
sync_quantities
|
|
@garden.update_column(:layout_saved_at, Time.current) # rubocop:disable Rails/SkipsModelValidations
|
|
true
|
|
end
|
|
return render json: { errors: @layout_errors }, status: :unprocessable_content unless saved
|
|
|
|
render json: GardenLayoutSerializer.new(@garden.reload, editable: true, resizable: can?(:update, @garden)).as_json
|
|
end
|
|
|
|
def new
|
|
@garden = Garden.new
|
|
return render json: GardenFormSerializer.new(@garden, member: current_member) if request.format.json?
|
|
|
|
respond_with(@garden)
|
|
end
|
|
|
|
def edit
|
|
return render json: GardenFormSerializer.new(@garden, member: current_member) if request.format.json?
|
|
|
|
respond_with(@garden)
|
|
end
|
|
|
|
def create
|
|
@garden.owner_id = current_member.id
|
|
return render_card_json(@garden.save) if request.format.json?
|
|
|
|
if @garden.save
|
|
link = new_job_path(name: 'Weed the garden bed', garden_id: @garden.id, due_date: 2.weeks.from_now.to_date)
|
|
flash[:notice] = t('gardens.created_prompt_html', link: link).html_safe
|
|
end
|
|
respond_with(@garden)
|
|
end
|
|
|
|
def update
|
|
saved = @garden.update(garden_params)
|
|
return render_card_json(saved) if request.format.json?
|
|
|
|
flash[:notice] = I18n.t('gardens.updated') if saved
|
|
respond_with(@garden)
|
|
end
|
|
|
|
def destroy
|
|
@garden.destroy
|
|
flash[:notice] = I18n.t('gardens.deleted')
|
|
redirect_to(member_gardens_path(@garden.owner))
|
|
end
|
|
|
|
# Lets a member use a garden they already have as their nursery.
|
|
def make_nursery
|
|
@garden.make_nursery!
|
|
redirect_to @garden, notice: t('gardens.nursery_made')
|
|
end
|
|
|
|
def stop_nursery
|
|
@garden.update!(nursery: false)
|
|
redirect_to @garden, notice: t('gardens.nursery_stopped')
|
|
end
|
|
|
|
def fetch_wikidata
|
|
if @garden.populate_wikidata_info
|
|
@garden.save
|
|
flash[:notice] = "Wikidata information updated."
|
|
else
|
|
flash[:alert] = "Could not find Wikidata information for this location."
|
|
end
|
|
redirect_to @garden
|
|
end
|
|
|
|
private
|
|
|
|
# A member who can look after the garden gets a to-do list of their own jobs
|
|
# for it. Everyone else sees the jobs people have planned there, as cards.
|
|
def load_jobs
|
|
if member_signed_in? && can?(:edit, @garden)
|
|
@notepad = JobsNotepad.new(current_member, garden: @garden)
|
|
else
|
|
@current_jobs = @garden.jobs.active.includes(:owner).order(created_at: :desc)
|
|
@finished_jobs = @garden.jobs.finished.includes(:owner).order(created_at: :desc)
|
|
end
|
|
end
|
|
|
|
# Takes every plant off the grid first, so the new arrangement is checked
|
|
# against a clean bed and two plants can swap cells in one save.
|
|
#
|
|
# The callers read the plants back afterwards rather than clearing the copies
|
|
# they already hold. Assigning nil to a loaded record and then assigning its
|
|
# old position back leaves it unchanged as far as dirty tracking is concerned,
|
|
# so the save would do nothing and the plant would stay off the grid — which
|
|
# is only visible as every plant but the one just dragged disappearing.
|
|
def clear_positions
|
|
Plant.where(planting_id: @garden.plantings.current.select(:id))
|
|
.update_all(bed_x: nil, bed_y: nil) # rubocop:disable Rails/SkipsModelValidations
|
|
end
|
|
|
|
# Collects the errors of any placement that wouldn't save, for the response.
|
|
def apply_placements(placements)
|
|
existing = @garden.placed_or_owned_plants.index_by(&:id)
|
|
# Plants this request already accounts for, so pulling from a stack doesn't
|
|
# grab one that another placement is about to use.
|
|
spoken_for = placements.filter_map { |placement| placement[:plant_id].presence&.to_i }.to_set
|
|
|
|
placements.each do |placement|
|
|
plant = plant_for(placement, existing, spoken_for)
|
|
next if plant.nil? # the error is already recorded
|
|
|
|
plant.assign_attributes(placement.slice(:bed_x, :bed_y, :diameter))
|
|
@layout_errors[plant.id || :new] = plant.errors.full_messages unless plant.save
|
|
end
|
|
end
|
|
|
|
# A placement names either a plant already on the bed, or just the planting it
|
|
# came off, which means one more of that crop.
|
|
def plant_for(placement, existing, spoken_for)
|
|
return known_plant(placement, existing) if placement[:plant_id].present?
|
|
|
|
planting = @garden.plantings.current.find_by(id: placement[:planting_id])
|
|
if planting.nil?
|
|
@layout_errors[placement[:planting_id]] = ['is not a planting in this garden']
|
|
return nil
|
|
end
|
|
|
|
take_from_stack(planting, spoken_for)
|
|
end
|
|
|
|
def known_plant(placement, existing)
|
|
plant = existing[placement[:plant_id].to_i]
|
|
@layout_errors[placement[:plant_id]] = ['is not a plant in this garden'] if plant.nil?
|
|
plant
|
|
end
|
|
|
|
# Uses up a plant the planting already has before making another, so a
|
|
# planting that says it has nine tomatoes maps those nine first.
|
|
def take_from_stack(planting, spoken_for)
|
|
spare = planting.plants.find { |plant| spoken_for.exclude?(plant.id) }
|
|
spoken_for << spare.id if spare
|
|
spare || planting.plants.build
|
|
end
|
|
|
|
# Composting is the one way the layout makes a planting smaller: the plant
|
|
# is gone, rather than lifted off the bed, and sync_quantities then counts
|
|
# one fewer.
|
|
def compost(plant_ids)
|
|
return if plant_ids.empty?
|
|
|
|
plants = @garden.placed_or_owned_plants.where(id: plant_ids)
|
|
(plant_ids - plants.ids).each { |id| @layout_errors[id] = ['is not a plant in this garden'] }
|
|
plants.destroy_all
|
|
end
|
|
|
|
# Stones, labels and rows are saved as a list on the garden, checked here and
|
|
# written directly: a full save would geocode the garden on every drag.
|
|
def apply_features(features)
|
|
problems = @garden.layout_feature_problems(features)
|
|
return @layout_errors[:features] = problems if problems.any?
|
|
|
|
@garden.update_column(:layout_features, features) # rubocop:disable Rails/SkipsModelValidations
|
|
end
|
|
|
|
# The number of plants is whatever ended up on and off the bed, so dragging
|
|
# one more onto the grid is what makes the planting bigger.
|
|
def sync_quantities
|
|
@garden.plantings.current.includes(:plants).find_each do |planting|
|
|
count = planting.plants.size
|
|
planting.update_column(:quantity, count) if planting.quantity != count # rubocop:disable Rails/SkipsModelValidations
|
|
end
|
|
end
|
|
|
|
# Permits each placement on its own rather than the whole params hash: the
|
|
# route's :member_slug and :slug, and the empty :garden that wrap_parameters
|
|
# adds to JSON requests, would otherwise count as unpermitted and raise.
|
|
def layout_params
|
|
placements = params[:placements]
|
|
return [] unless placements.is_a?(Array)
|
|
|
|
placements.map do |placement|
|
|
placement.permit(:plant_id, :planting_id, :bed_x, :bed_y, :diameter).to_h.symbolize_keys
|
|
end
|
|
end
|
|
|
|
# The stones, labels and rows, as plain hashes with numbers as numbers. Nil
|
|
# when the request doesn't send them, which leaves them as they are.
|
|
def features_params
|
|
features = params[:features]
|
|
return nil if features.nil?
|
|
return [] unless features.is_a?(Array)
|
|
|
|
features.map do |feature|
|
|
hash = feature.permit(:id, :kind, :x, :y, :x2, :y2, :text).to_h
|
|
%w(x y x2 y2).each { |key| hash[key] = Float(hash[key], exception: false) if hash.key?(key) }
|
|
hash.compact
|
|
end
|
|
end
|
|
|
|
# The ids of plants dropped in the compost bin.
|
|
def compost_params
|
|
ids = params[:composted]
|
|
return [] unless ids.is_a?(Array)
|
|
|
|
ids.filter_map { |id| Integer(id.to_s, exception: false) }
|
|
end
|
|
|
|
# Used by the React garden cards, which replace the garden's card with the
|
|
# returned one. No flash: nothing redirects, so it would turn up on the next page.
|
|
def render_card_json(saved)
|
|
if saved
|
|
card = GardenCardSerializer.collection([@garden], ability: current_ability, show_owner: false).first
|
|
render json: { garden: card }
|
|
else
|
|
render json: { errors: @garden.errors }, status: :unprocessable_content
|
|
end
|
|
end
|
|
|
|
def garden_params
|
|
params.require(:garden).permit(
|
|
:name, :slug, :description, :active,
|
|
:location, :latitude, :longitude, :area, :area_unit, :garden_type_id,
|
|
:location_wikidata_id, :lowest_temp_c, :highest_temp_c,
|
|
:grid_columns, :grid_rows
|
|
)
|
|
end
|
|
end
|