From a98990ccd2d38e818f9b2be09ae2cfacf60774e9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 22:31:14 +0930 Subject: [PATCH] Add transplant feature for plantings (#4133) * Add ability to transplant a planting * Fix view tests * Transplantable gardens * Add spec --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Daniel O'Connor --- app/controllers/plantings_controller.rb | 26 +++++++++++++++++++ app/helpers/plantings_helper.rb | 8 ++++++ app/models/ability.rb | 4 +++ app/models/garden.rb | 2 ++ app/views/plantings/_actions.html.haml | 8 ++++++ config/routes.rb | 3 +++ db/schema.rb | 2 +- .../plantings/planting_a_crop_spec.rb | 23 ++++++++++++++++ 8 files changed, 75 insertions(+), 1 deletion(-) diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index c1b3bb8fc..e389a53ea 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -91,6 +91,32 @@ class PlantingsController < DataController respond_with @planting, location: @planting.garden end + def transplant + # The `load_and_authorize_resource` in DataController will handle finding the + # planting and authorizing the action. + # We still need to authorize the new garden + new_garden = Garden.find(params[:garden_id]) + authorize! :update, new_garden + + # Mark original planting as finished + @planting.update(finished: true, finished_at: Time.zone.now) + + # Create a new planting + new_planting = @planting.dup + new_planting.garden = new_garden + new_planting.slug = nil # let friendly_id generate a new slug + new_planting.finished = false + new_planting.finished_at = nil + + if new_planting.save + redirect_to edit_planting_path(new_planting), notice: 'Planting was successfully transplanted.' + else + # if the save fails, we should probably roll back the finishing of the original planting + @planting.update(finished: false, finished_at: nil) + redirect_to @planting, alert: "There was an error transplanting the planting: #{new_planting.errors.full_messages.to_sentence}" + end + end + private def update_crop_medians diff --git a/app/helpers/plantings_helper.rb b/app/helpers/plantings_helper.rb index 3b977f656..1d5345151 100644 --- a/app/helpers/plantings_helper.rb +++ b/app/helpers/plantings_helper.rb @@ -43,6 +43,14 @@ module PlantingsHelper (planting.first_harvest_predicted_at - Time.zone.today).to_i end + # Returns a list of gardens the planting can be transplanted to + # based on the planting's owner. + def transplantable_gardens_by_owner(planting) + garden_ids = planting.owner.gardens.select(:id).to_a + GardenCollaborator.where(member_id: planting.owner.id).select(:garden_id).to_a + + Garden.active.where.not(id: planting.garden_id).where(id: garden_ids) + end + def days_from_now_to_last_harvest(planting) return unless planting.planted_at.present? && planting.last_harvest_predicted_at.present? diff --git a/app/models/ability.rb b/app/models/ability.rb index 7c32ea241..9d80af4cf 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -111,6 +111,10 @@ class Ability can :update, Planting do |planting| planting.garden.garden_collaborators.where(member_id: member.id).any? end + can :transplant, Planting, garden: { owner_id: member.id } + can :transplant, Planting do |planting| + planting.garden.garden_collaborators.where(member_id: member.id).any? + end can :destroy, Planting do |planting| planting.garden.garden_collaborators.where(member_id: member.id).any? end diff --git a/app/models/garden.rb b/app/models/garden.rb index 5ef0adea3..5d3f04adb 100644 --- a/app/models/garden.rb +++ b/app/models/garden.rb @@ -5,6 +5,7 @@ class Garden < ApplicationRecord include Geocodable include PhotoCapable include Ownable + friendly_id :garden_slug, use: %i(slugged finders) has_many :plantings, dependent: :destroy @@ -44,6 +45,7 @@ class Garden < ApplicationRecord .where.not(gardens: { latitude: nil }) .where.not(gardens: { longitude: nil }) } + AREA_UNITS_VALUES = { "square metres" => "square metre", "square feet" => "square foot", diff --git a/app/views/plantings/_actions.html.haml b/app/views/plantings/_actions.html.haml index d903d00d9..4a1d8b10b 100644 --- a/app/views/plantings/_actions.html.haml +++ b/app/views/plantings/_actions.html.haml @@ -9,5 +9,13 @@ = planting_finish_button(planting, classes: 'dropdown-item') = planting_harvest_button(planting, classes: 'dropdown-item') = planting_save_seeds_button(planting, classes: 'dropdown-item') + - if can?(:transplant, planting) && planting.active && transplantable_gardens_by_owner(planting).any? + .dropdown-divider + .px-2 + = form_tag transplant_planting_path(planting), method: :post do + .form-group + = label_tag :garden_id, 'Transplant to:' + = select_tag :garden_id, options_from_collection_for_select(transplantable_gardens_by_owner(planting), :id, :name), class: 'form-control form-control-sm' + = submit_tag 'Transplant', class: 'btn btn-sm btn-primary mt-2' .dropdown-divider = delete_button(planting, classes: 'dropdown-item text-danger') diff --git a/config/routes.rb b/config/routes.rb index 1e689a37c..45e68fcb1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -40,6 +40,9 @@ Rails.application.routes.draw do collection do get 'crop/:crop' => 'plantings#index', as: 'plantings_by_crop' end + member do + post :transplant + end end resources :seeds, concerns: :has_photos, param: :slug do diff --git a/db/schema.rb b/db/schema.rb index 6d78e9535..df39805ca 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -65,9 +65,9 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_085224) do t.string "name", null: false t.integer "crop_id", null: false t.integer "creator_id", null: false - t.string "language", null: false t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil + t.string "language" end create_table "authentications", id: :serial, force: :cascade do |t| diff --git a/spec/features/plantings/planting_a_crop_spec.rb b/spec/features/plantings/planting_a_crop_spec.rb index 68032bb43..28fc3981d 100644 --- a/spec/features/plantings/planting_a_crop_spec.rb +++ b/spec/features/plantings/planting_a_crop_spec.rb @@ -233,6 +233,29 @@ describe "Planting a crop", :js, :search do expect(page).to have_content "maize" end + describe "Transplanting a planting" do + it "allows transplanting to another garden" do + other_garden = FactoryBot.create(:garden, owner: member, name: 'Backyard') + visit planting_path(planting) + click_link 'Actions' + select other_garden.name, from: 'Transplant to:' + click_on "Transplant" + expect(page).to have_content "Planting was successfully transplanted" + + new_planting = Planting.last + planting.reload + + # The old planting is finished. + expect(planting.finished).to be true + expect(planting.finished_at).not_to be_nil + + # The new planting is a continuation of the old one. + expect(new_planting.garden).to eq(other_garden) + expect(new_planting.crop).to eq(planting.crop) + expect(new_planting.owner).to eq(planting.owner) + end + end + describe "Marking a planting as finished without a date" do before do fill_autocomplete "crop", with: "mai"