From 0079513b35b210dcd90f8a6f795daccf7c33b282 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 19:51:24 +0930 Subject: [PATCH 001/387] Merge pull request #4183 from Growstuff/feature/timeline-likes Feature: Display likes on timeline --- app/helpers/event_helper.rb | 2 ++ app/services/timeline_service.rb | 10 ++++++++++ app/views/likes/_description.html.haml | 1 + app/views/timeline/_like.html.haml | 2 ++ app/views/timeline/index.html.haml | 1 + app/views/timeline/likeables/_photo.html.haml | 1 + app/views/timeline/likeables/_post.html.haml | 6 ++++++ spec/features/timeline/index_spec.rb | 6 ++++++ 8 files changed, 29 insertions(+) create mode 100644 app/views/likes/_description.html.haml create mode 100644 app/views/timeline/_like.html.haml create mode 100644 app/views/timeline/likeables/_photo.html.haml create mode 100644 app/views/timeline/likeables/_post.html.haml diff --git a/app/helpers/event_helper.rb b/app/helpers/event_helper.rb index 4bc8033b5..2e127c319 100644 --- a/app/helpers/event_helper.rb +++ b/app/helpers/event_helper.rb @@ -7,6 +7,8 @@ module EventHelper def event_description(event) render "#{event.event_type.pluralize}/description", event_model: resolve_model(event) +rescue ActionView::MissingTemplate + "#{event.event_type.humanize.downcase}d" end def resolve_model(event) diff --git a/app/services/timeline_service.rb b/app/services/timeline_service.rb index 25c5706df..3b813bc5b 100644 --- a/app/services/timeline_service.rb +++ b/app/services/timeline_service.rb @@ -18,10 +18,20 @@ class TimelineService .union_all(photos_query) .union_all(seeds_query) .union_all(activities_query) + .union_all(likes_query) .where.not(event_at: nil) .order(event_at: :desc) end + def self.likes_query + Like + .select("likes.id", + "'like' as event_type", + "likes.created_at as event_at", + "likes.member_id as owner_id", + "null as crop_id") + end + def self.activities_query Activity.select( :id, diff --git a/app/views/likes/_description.html.haml b/app/views/likes/_description.html.haml new file mode 100644 index 000000000..d8427969d --- /dev/null +++ b/app/views/likes/_description.html.haml @@ -0,0 +1 @@ +#{link_to event_model.member, event_model.member} liked #{link_to event_model.likeable.class.name.downcase, event_model.likeable} diff --git a/app/views/timeline/_like.html.haml b/app/views/timeline/_like.html.haml new file mode 100644 index 000000000..fa1f6bb44 --- /dev/null +++ b/app/views/timeline/_like.html.haml @@ -0,0 +1,2 @@ +- likeable = like.likeable += render "timeline/likeables/#{likeable.class.name.downcase}", likeable: likeable diff --git a/app/views/timeline/index.html.haml b/app/views/timeline/index.html.haml index 3285db15b..165f9f977 100644 --- a/app/views/timeline/index.html.haml +++ b/app/views/timeline/index.html.haml @@ -14,6 +14,7 @@ = link_to owner, owner = event_description(event) = render 'timeline/photos', photo: resolve_model(event) if event.event_type == 'photo' + = render 'timeline/like', like: resolve_model(event) if event.event_type == 'like' %small - if event.event_at.present? - if event.event_at.kind_of?(Date) diff --git a/app/views/timeline/likeables/_photo.html.haml b/app/views/timeline/likeables/_photo.html.haml new file mode 100644 index 000000000..1b680a22e --- /dev/null +++ b/app/views/timeline/likeables/_photo.html.haml @@ -0,0 +1 @@ += render 'timeline/photos', photo: likeable diff --git a/app/views/timeline/likeables/_post.html.haml b/app/views/timeline/likeables/_post.html.haml new file mode 100644 index 000000000..5cd54af3d --- /dev/null +++ b/app/views/timeline/likeables/_post.html.haml @@ -0,0 +1,6 @@ +.card.my-2 + .card-body + %blockquote.blockquote.mb-0 + %p= truncate(likeable.body, length: 140) + %footer.blockquote-footer + = link_to "view post", likeable diff --git a/spec/features/timeline/index_spec.rb b/spec/features/timeline/index_spec.rb index 3135bf65a..c80e74305 100644 --- a/spec/features/timeline/index_spec.rb +++ b/spec/features/timeline/index_spec.rb @@ -17,6 +17,10 @@ describe "timeline", :js do let!(:friend_harvest) { FactoryBot.create(:planting, owner: friend2, planted_at: 3.years.ago) } let!(:finished_planting) { FactoryBot.create(:finished_planting, owner: friend1) } let!(:no_planted_at_planting) { FactoryBot.create(:planting, owner: friend2, planted_at: nil) } + let!(:friend_photo) { FactoryBot.create(:photo, owner: friend1) } + let!(:friend_post) { FactoryBot.create(:post, author: friend2) } + let!(:liked_post) { FactoryBot.create(:like, likeable: friend_photo, member: friend2) } + let!(:liked_photo) { FactoryBot.create(:like, likeable: friend_post, member: friend1) } before do login_as(member) @@ -28,6 +32,8 @@ describe "timeline", :js do it { expect(page).to have_link href: planting_path(friend_harvest) } it { expect(page).to have_link href: planting_path(finished_planting) } it { expect(page).to have_no_link href: planting_path(no_planted_at_planting) } + it { expect(page).to have_link href: photo_path(friend_photo) } + it { expect(page).to have_link href: post_path(friend_post) } end describe 'shows the friends you follow' do From a4db05c0f6ff4f78c923a334e653d0619c19ac49 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 1 Sep 2025 11:25:02 +0000 Subject: [PATCH 002/387] Add a lot of indexes --- .../20250901110545_add_indexes_crops.rb | 53 +++++++++++++ db/schema.rb | 78 ++++++++++++++++++- 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20250901110545_add_indexes_crops.rb diff --git a/db/migrate/20250901110545_add_indexes_crops.rb b/db/migrate/20250901110545_add_indexes_crops.rb new file mode 100644 index 000000000..80d6b522d --- /dev/null +++ b/db/migrate/20250901110545_add_indexes_crops.rb @@ -0,0 +1,53 @@ +class AddIndexesCrops < ActiveRecord::Migration[7.2] + def change + add_index :alternate_names, :crop_id + add_index :alternate_names, :creator_id + add_index :alternate_names, :language + + add_index :comments, %i(commentable_type commentable_id) + add_index :comments, :author_id + + add_index :crop_companions, %i(crop_a_id crop_b_id) + + add_index :crops, :creator_id + add_index :crops, :parent_id + + add_index :follows, %i(follower_id followed_id) + + add_index :forums, :owner_id + + add_index :harvests, :crop_id + add_index :harvests, :owner_id + add_index :harvests, :plant_part_id + + add_index :members_roles, %i(member_id role_id) + + add_index :notifications, :sender_id + add_index :notifications, :recipient_id + + add_index :orders_products, %i(order_id product_id) + + add_index :photo_associations, :crop_id # TODO: Is this still in use? + + add_index :photos, :owner_id + add_index :photos, :source_id + + add_index :photos_plantings, %i(photo_id planting_id) + + add_index :plant_parts, :slug, unique: true + + add_index :plantings, :crop_id + add_index :plantings, :garden_id + add_index :plantings, :owner_id + add_index :plantings, :parent_seed_id + + add_index :posts, :forum_id + + add_index :scientific_names, :crop_id + add_index :scientific_names, :creator_id + + add_index :seeds, :owner_id + add_index :seeds, :crop_id + add_index :seeds, :parent_planting_id + end +end diff --git a/db/schema.rb b/db/schema.rb index f96175d6e..6c279b9a9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do +ActiveRecord::Schema[7.2].define(version: 2025_09_01_110545) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -68,6 +68,9 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil t.string "language" + t.index ["creator_id"], name: "index_alternate_names_on_creator_id" + t.index ["crop_id"], name: "index_alternate_names_on_crop_id" + t.index ["language"], name: "index_alternate_names_on_language" end create_table "authentications", id: :serial, force: :cascade do |t| @@ -209,6 +212,8 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil t.string "commentable_type" + t.index ["author_id"], name: "index_comments_on_author_id" + t.index ["commentable_type", "commentable_id"], name: "index_comments_on_commentable_type_and_commentable_id" end create_table "crop_companions", force: :cascade do |t| @@ -216,6 +221,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.integer "crop_b_id", null: false t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false + t.index ["crop_a_id", "crop_b_id"], name: "index_crop_companions_on_crop_a_id_and_crop_b_id" end create_table "crop_posts", id: false, force: :cascade do |t| @@ -246,7 +252,9 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.jsonb "openfarm_data" t.integer "harvests_count", default: 0 t.integer "photo_associations_count", default: 0 + t.index ["creator_id"], name: "index_crops_on_creator_id" t.index ["name"], name: "index_crops_on_name" + t.index ["parent_id"], name: "index_crops_on_parent_id" t.index ["requester_id"], name: "index_crops_on_requester_id" t.index ["slug"], name: "index_crops_on_slug", unique: true end @@ -256,6 +264,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.integer "followed_id" t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil + t.index ["follower_id", "followed_id"], name: "index_follows_on_follower_id_and_followed_id" end create_table "forums", id: :serial, force: :cascade do |t| @@ -265,6 +274,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.datetime "created_at", precision: nil t.datetime "updated_at", precision: nil t.string "slug" + t.index ["owner_id"], name: "index_forums_on_owner_id" t.index ["slug"], name: "index_forums_on_slug", unique: true end @@ -328,6 +338,9 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.float "si_weight" t.integer "planting_id" t.integer "likes_count", default: 0 + t.index ["crop_id"], name: "index_harvests_on_crop_id" + t.index ["owner_id"], name: "index_harvests_on_owner_id" + t.index ["plant_part_id"], name: "index_harvests_on_plant_part_id" t.index ["planting_id"], name: "index_harvests_on_planting_id" end @@ -464,6 +477,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do create_table "members_roles", id: false, force: :cascade do |t| t.integer "member_id" t.integer "role_id" + t.index ["member_id", "role_id"], name: "index_members_roles_on_member_id_and_role_id" end create_table "notifications", id: :serial, force: :cascade do |t| @@ -477,11 +491,14 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.datetime "updated_at", precision: nil t.string "notifiable_type" t.index ["notifiable_type", "notifiable_id"], name: "index_notifications_on_notifiable_type_and_notifiable_id" + t.index ["recipient_id"], name: "index_notifications_on_recipient_id" + t.index ["sender_id"], name: "index_notifications_on_sender_id" end create_table "orders_products", id: false, force: :cascade do |t| t.integer "order_id" t.integer "product_id" + t.index ["order_id", "product_id"], name: "index_orders_products_on_order_id_and_product_id" end create_table "photo_associations", id: :serial, force: :cascade do |t| @@ -491,6 +508,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.integer "crop_id" + t.index ["crop_id"], name: "index_photo_associations_on_crop_id" t.index ["photographable_id", "photographable_type", "photo_id"], name: "items_to_photos_idx", unique: true t.index ["photographable_id", "photographable_type"], name: "photographable_idx" end @@ -511,12 +529,15 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.string "source" t.integer "comments_count", default: 0 t.index ["fullsize_url"], name: "index_photos_on_fullsize_url", unique: true + t.index ["owner_id"], name: "index_photos_on_owner_id" + t.index ["source_id"], name: "index_photos_on_source_id" t.index ["thumbnail_url"], name: "index_photos_on_thumbnail_url", unique: true end create_table "photos_plantings", id: false, force: :cascade do |t| t.integer "photo_id" t.integer "planting_id" + t.index ["photo_id", "planting_id"], name: "index_photos_plantings_on_photo_id_and_planting_id" end create_table "photos_seeds", id: false, force: :cascade do |t| @@ -531,6 +552,17 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.datetime "updated_at", precision: nil t.string "slug" t.integer "harvests_count", default: 0 + t.index ["slug"], name: "index_plant_parts_on_slug", unique: true + end + + create_table "planting_problems", force: :cascade do |t| + t.bigint "planting_id" + t.bigint "problem_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["planting_id", "problem_id"], name: "index_planting_problems_on_planting_id_and_problem_id", unique: true + t.index ["planting_id"], name: "index_planting_problems_on_planting_id" + t.index ["problem_id"], name: "index_planting_problems_on_problem_id" end create_table "plantings", id: :serial, force: :cascade do |t| @@ -554,6 +586,10 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.integer "harvests_count", default: 0 t.integer "likes_count", default: 0 t.boolean "failed", default: false, null: false + t.index ["crop_id"], name: "index_plantings_on_crop_id" + t.index ["garden_id"], name: "index_plantings_on_garden_id" + t.index ["owner_id"], name: "index_plantings_on_owner_id" + t.index ["parent_seed_id"], name: "index_plantings_on_parent_seed_id" t.index ["slug"], name: "index_plantings_on_slug", unique: true end @@ -568,9 +604,36 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.integer "likes_count", default: 0 t.integer "comments_count", default: 0 t.index ["created_at", "author_id"], name: "index_posts_on_created_at_and_author_id" + t.index ["forum_id"], name: "index_posts_on_forum_id" t.index ["slug"], name: "index_posts_on_slug", unique: true end + create_table "problem_posts", force: :cascade do |t| + t.bigint "problem_id" + t.bigint "post_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["post_id"], name: "index_problem_posts_on_post_id" + t.index ["problem_id", "post_id"], name: "index_problem_posts_on_problem_id_and_post_id", unique: true + t.index ["problem_id"], name: "index_problem_posts_on_problem_id" + end + + create_table "problems", force: :cascade do |t| + t.string "name" + t.string "reason_for_rejection" + t.string "rejection_notes" + t.string "approval_status", default: "pending", null: false + t.bigint "requester_id" + t.bigint "creator_id" + t.string "slug" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["creator_id"], name: "index_problems_on_creator_id" + t.index ["name"], name: "index_problems_on_name" + t.index ["requester_id"], name: "index_problems_on_requester_id" + t.index ["slug"], name: "index_problems_on_slug" + end + create_table "roles", id: :serial, force: :cascade do |t| t.string "name", null: false t.text "description" @@ -590,6 +653,8 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.string "gbif_rank" t.string "gbif_status" t.string "wikidata_id" + t.index ["creator_id"], name: "index_scientific_names_on_creator_id" + t.index ["crop_id"], name: "index_scientific_names_on_crop_id" end create_table "seeds", id: :serial, force: :cascade do |t| @@ -611,7 +676,12 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do t.date "finished_at" t.integer "parent_planting_id" t.date "saved_at" + t.string "source" + t.index ["crop_id"], name: "index_seeds_on_crop_id" + t.index ["owner_id"], name: "index_seeds_on_owner_id" + t.index ["parent_planting_id"], name: "index_seeds_on_parent_planting_id" t.index ["slug"], name: "index_seeds_on_slug", unique: true + t.index ["source"], name: "index_seeds_on_source" end add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" @@ -622,6 +692,12 @@ ActiveRecord::Schema[7.2].define(version: 2025_08_24_162600) do add_foreign_key "mailboxer_receipts", "mailboxer_notifications", column: "notification_id", name: "receipts_on_notification_id" add_foreign_key "photo_associations", "crops" add_foreign_key "photo_associations", "photos" + add_foreign_key "planting_problems", "plantings" + add_foreign_key "planting_problems", "problems" add_foreign_key "plantings", "seeds", column: "parent_seed_id", name: "parent_seed", on_delete: :nullify + add_foreign_key "problem_posts", "posts" + add_foreign_key "problem_posts", "problems" + add_foreign_key "problems", "members", column: "creator_id" + add_foreign_key "problems", "members", column: "requester_id" add_foreign_key "seeds", "plantings", column: "parent_planting_id", name: "parent_planting", on_delete: :nullify end From d185ce495f6c3cd2aa7c5a6911c8ff0456eefc69 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 1 Sep 2025 12:13:08 +0000 Subject: [PATCH 003/387] Remove haml-lint-extractor --- Gemfile | 1 - Gemfile.lock | 8 -------- 2 files changed, 9 deletions(-) diff --git a/Gemfile b/Gemfile index c7259f926..bd7ef2466 100644 --- a/Gemfile +++ b/Gemfile @@ -178,7 +178,6 @@ group :development, :test do gem 'dotenv-rails' # cli utils - gem 'haml-i18n-extractor', require: false gem 'haml_lint', '>= 0.25.1', require: false # Checks haml files for goodness gem 'i18n-tasks', require: false # adds tests for finding missing and unused translations gem 'rspectre', require: false # finds unused code in specs diff --git a/Gemfile.lock b/Gemfile.lock index f25fd5699..2509c7f34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -294,12 +294,6 @@ GEM temple (>= 0.8.2) thor tilt - haml-i18n-extractor (0.5.9) - activesupport - haml - highline - tilt - trollop (= 1.16.2) haml-rails (2.1.0) actionpack (>= 5.1) activesupport (>= 5.1) @@ -701,7 +695,6 @@ GEM tilt (2.6.1) timecop (0.9.10) timeout (0.4.3) - trollop (1.16.2) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (3.1.5) @@ -788,7 +781,6 @@ DEPENDENCIES gibbon (~> 1.2.0) gravatar-ultimate haml - haml-i18n-extractor haml-rails haml_lint (>= 0.25.1) hashie (>= 3.5.3) From 0f4803392d7b0ea09ed1428c6bcc35e39da1e661 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 21:47:31 +0930 Subject: [PATCH 004/387] Add seed source to Seed model (#4186) * Add seed source to Seed model * Update _form.html.haml * Add to schema * Default option * Default option * Fix test --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Daniel O'Connor --- app/controllers/seeds_controller.rb | 4 +++- app/models/seed.rb | 5 +++++ app/views/seeds/_form.html.haml | 16 +++++++++------- db/migrate/20250901105232_add_source_to_seeds.rb | 6 ++++++ spec/features/seeds/misc_seeds_spec.rb | 1 + 5 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 db/migrate/20250901105232_add_source_to_seeds.rb diff --git a/app/controllers/seeds_controller.rb b/app/controllers/seeds_controller.rb index 6a5b33517..6dd2741cd 100644 --- a/app/controllers/seeds_controller.rb +++ b/app/controllers/seeds_controller.rb @@ -43,6 +43,7 @@ class SeedsController < DataController def new @seed = Seed.new + @seed.source = 'my own seed saving' if params[:planting_slug] @planting = Planting.find_by(slug: params[:planting_slug]) @@ -56,6 +57,7 @@ class SeedsController < DataController def create @seed = Seed.new(seed_params) + @seed.source ||= 'my own seed saving' @seed.finished ||= false @seed.owner = current_member @seed.crop = @seed.parent_planting.crop if @seed.parent_planting @@ -84,7 +86,7 @@ class SeedsController < DataController :crop_id, :description, :quantity, :plant_before, :parent_planting_id, :saved_at, :days_until_maturity_min, :days_until_maturity_max, - :organic, :gmo, + :organic, :gmo, :source, :heirloom, :tradable_to, :slug, :finished, :finished_at ) diff --git a/app/models/seed.rb b/app/models/seed.rb index 92050be57..e99429a44 100644 --- a/app/models/seed.rb +++ b/app/models/seed.rb @@ -12,6 +12,8 @@ class Seed < ApplicationRecord ORGANIC_VALUES = ['certified organic', 'non-certified organic', 'conventional/non-organic', 'unknown'].freeze GMO_VALUES = ['certified GMO-free', 'non-certified GMO-free', 'GMO', 'unknown'].freeze HEIRLOOM_VALUES = %w(heirloom hybrid unknown).freeze + SOURCE_VALUES = ['seed catalogue', 'retail outlet', 'seed bank or similar institution', + 'traded from another person', 'my own seed saving', 'other/unknown'].freeze # # Relationships @@ -44,6 +46,9 @@ class Seed < ApplicationRecord validates :heirloom, allow_blank: false, inclusion: { in: HEIRLOOM_VALUES, message: "You must say whether the seeds" \ "are heirloom, hybrid, or unknown" } + validates :source, allow_blank: true, + inclusion: { in: SOURCE_VALUES, message: "You must say where the seeds are from," \ + "or that you don't know" } # # Delegations diff --git a/app/views/seeds/_form.html.haml b/app/views/seeds/_form.html.haml index 02b0f4358..f423c6507 100644 --- a/app/views/seeds/_form.html.haml +++ b/app/views/seeds/_form.html.haml @@ -49,17 +49,19 @@ .col-md-6= f.number_field :days_until_maturity_max, label_as_placeholder: true, label: 'max', prepend: 'to', append: "days", min: 1 .row - .col-md-4 - = f.select(:organic, Seed::ORGANIC_VALUES, {label: 'Organic?', wrapper: { class: 'required'}, required: true}, default: 'unknown') - .col-md-4 - = f.select(:gmo, Seed::GMO_VALUES, {label: 'GMO?', wrapper: { class: 'required'}, required: true}, default: 'unknown') - .col-md-4 - = f.select(:heirloom, Seed::HEIRLOOM_VALUES, {label: 'Heirloom?', wrapper: { class: 'required'}, required: true}, default: 'unknown') + .col-md-3 + = f.select(:organic, Seed::ORGANIC_VALUES, { label: 'Organic?', wrapper: { class: 'required' }, required: true }, default: 'unknown') + .col-md-3 + = f.select(:gmo, Seed::GMO_VALUES, { label: 'GMO?', wrapper: { class: 'required' }, required: true }, default: 'unknown') + .col-md-3 + = f.select(:heirloom, Seed::HEIRLOOM_VALUES, { label: 'Heirloom?', wrapper: { class: 'required' }, required: true }, default: 'unknown') + .col-md-3 + = f.select(:source, Seed::SOURCE_VALUES, { label: 'Source?', wrapper: { class: 'required' }, required: true }, default: 'unknown') = f.text_area :description, rows: 6 %hr/ = t('.trade_help', site_name: ENV['GROWSTUFF_SITE_NAME']) - = f.select(:tradable_to, Seed::TRADABLE_TO_VALUES, {label: 'Will trade', wrapper: { class: 'required'}, required: true}) + = f.select(:tradable_to, Seed::TRADABLE_TO_VALUES, { label: 'Will trade', wrapper: { class: 'required' }, required: true }) %span.help_inline - if current_member.location.blank? Don't forget to diff --git a/db/migrate/20250901105232_add_source_to_seeds.rb b/db/migrate/20250901105232_add_source_to_seeds.rb new file mode 100644 index 000000000..2fca4b203 --- /dev/null +++ b/db/migrate/20250901105232_add_source_to_seeds.rb @@ -0,0 +1,6 @@ +class AddSourceToSeeds < ActiveRecord::Migration[7.2] + def change + add_column :seeds, :source, :string + add_index :seeds, :source + end +end diff --git a/spec/features/seeds/misc_seeds_spec.rb b/spec/features/seeds/misc_seeds_spec.rb index 24a8b3a78..4b52ac539 100644 --- a/spec/features/seeds/misc_seeds_spec.rb +++ b/spec/features/seeds/misc_seeds_spec.rb @@ -49,6 +49,7 @@ describe "seeds", :js do click_link 'Edit' expect(page).to have_current_path edit_seed_path(seed), ignore_query: true fill_in 'Quantity', with: seed.quantity * 2 + select 'traded from another person', from: 'Source' click_button 'Save' expect(page).to have_current_path seed_path(seed), ignore_query: true end From 468e34a551a47ec9457e3888cb704510f855685f Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 1 Sep 2025 12:56:22 +0000 Subject: [PATCH 005/387] Remove openfarm service --- app/services/openfarm_service.rb | 108 ------------------------------- 1 file changed, 108 deletions(-) delete mode 100644 app/services/openfarm_service.rb diff --git a/app/services/openfarm_service.rb b/app/services/openfarm_service.rb deleted file mode 100644 index 11b3d1e2d..000000000 --- a/app/services/openfarm_service.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -BASE = 'https://openfarm.cc/api/v1/' -# BASE = 'http://127.0.0.1:3000/api/v1/' - -class OpenfarmService - def initialize - @cropbot = Member.find_by(login_name: 'cropbot') - end - - def import! - Crop.all.order(updated_at: :desc).each do |crop| - Rails.logger.debug { "#{crop.id}, #{crop.name}" } - update_crop(crop) if crop.valid? - end - end - - def update_crop(crop) - openfarm_record = fetch(crop.name) - if openfarm_record.present? && openfarm_record.is_a?(String) - Rails.logger.info(openfarm_record) - elsif openfarm_record.present? && openfarm_record.fetch('data', false) - crop.update! openfarm_data: openfarm_record.fetch('data', false) - save_companions(crop, openfarm_record) - save_photos(crop) - else - Rails.logger.debug "\tcrop not found on Open Farm" - crop.update!(openfarm_data: false) - end - end - - def save_companions(crop, openfarm_record) - companions = openfarm_record.fetch('data').fetch('relationships').fetch('companions').fetch('data') - crops = openfarm_record.fetch('included', []).select { |rec| rec["type"] == 'crops' } - CropCompanion.transaction do - companions.each do |com| - companion_crop_hash = crops.detect { |c| c.fetch('id') == com.fetch('id') } - companion_crop_name = companion_crop_hash.fetch('attributes').fetch('name').downcase - companion_crop = Crop.where('lower(name) = ?', companion_crop_name).first - companion_crop = Crop.create!(name: companion_crop_name, requester: @cropbot, approval_status: "pending") if companion_crop.nil? - crop.companions << companion_crop unless crop.companions.where(id: companion_crop.id).any? - end - end - end - - def save_photos(crop) - pictures = fetch_pictures(crop.name) - pictures.each do |picture| - data = picture.fetch('attributes') - Rails.logger.debug(data) - next unless data.fetch('image_url').start_with? 'http' - next if Photo.find_by(source_id: picture.fetch('id'), source: 'openfarm') - - photo = Photo.new( - source_id: picture.fetch('id'), - source: 'openfarm', - owner: @cropbot, - thumbnail_url: data.fetch('thumbnail_url'), - fullsize_url: data.fetch('image_url'), - title: 'Open Farm photo', - license_name: 'No rights reserved', - link_url: "https://openfarm.cc/en/crops/#{name_to_slug(crop.name)}" - ) - if photo.valid? - Photo.transaction do - photo.save - PhotoAssociation.find_or_create_by! photo:, photographable: crop - end - Rails.logger.debug { "\t saved photo #{photo.id} #{photo.source_id}" } - else - Rails.logger.warn "Photo not valid" - end - end - end - - def fetch(name) - conn.get("crops/#{name_to_slug(name)}.json").body - rescue NoMethodError - Rails.logger.debug "error fetching crop" - Rails.logger.debug "BODY: " - Rails.logger.debug body - end - - def name_to_slug(name) - CGI.escape(name.gsub(' ', '-').downcase) - end - - def fetch_all(page) - conn.get("crops.json?page=#{page}").body.fetch('data', {}) - end - - def fetch_pictures(name) - body = conn.get("crops/#{name_to_slug(name)}/pictures.json").body - body.fetch('data', false) - rescue StandardError - Rails.logger.debug "Error fetching photos" - Rails.logger.debug [] - end - - private - - def conn - Faraday.new BASE do |conn| - conn.response :json, content_type: /\bjson$/ - conn.adapter Faraday.default_adapter - end - end -end From 70e6c44d822daeda2d03f87eb1822c6b32c3d118 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 1 Sep 2025 13:20:04 +0000 Subject: [PATCH 006/387] Sign up, sign in don't need JS --- spec/features/signin_spec.rb | 2 +- spec/features/signup_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/signin_spec.rb b/spec/features/signin_spec.rb index e0680b191..f1488ba7b 100644 --- a/spec/features/signin_spec.rb +++ b/spec/features/signin_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "signin", :js do +describe "signin" do let(:member) { FactoryBot.create(:member) } let(:recipient) { FactoryBot.create(:member) } let(:wrangler) { FactoryBot.create(:crop_wrangling_member) } diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb index 7745370bc..68ba750a4 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "signup", :js do +describe "signup" do it "sign up for new account from top menubar" do visit crops_path # something other than front page, which has multiple signup links click_link 'Sign up' From c189e3b01a81d870b6e64a30b58a1b36cf274646 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 1 Sep 2025 23:56:13 +0930 Subject: [PATCH 007/387] Merge pull request #4062 from Growstuff/feature/planting-rating Add overall_rating to Plantings --- app/controllers/plantings_controller.rb | 2 +- app/models/planting.rb | 3 ++ app/views/harvests/_planting.haml | 5 +++ app/views/plantings/_facts.haml | 6 +++ app/views/plantings/_form.html.haml | 9 ++++ app/views/plantings/show.html.haml | 9 ++++ .../20250901110545_add_indexes_crops.rb | 1 - ...0901130830_add_overall_rating_plantings.rb | 5 +++ db/schema.rb | 45 +------------------ .../plantings/planting_a_crop_spec.rb | 2 + 10 files changed, 42 insertions(+), 45 deletions(-) create mode 100644 db/migrate/20250901130830_add_overall_rating_plantings.rb diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index fdf33b709..07a73cbce 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -133,7 +133,7 @@ class PlantingsController < DataController :crop_id, :description, :garden_id, :planted_at, :parent_seed_id, :quantity, :sunniness, :planted_from, :finished, - :finished_at, :failed + :finished_at, :failed, :overall_rating ) end diff --git a/app/models/planting.rb b/app/models/planting.rb index 381a739c0..9f05a7732 100644 --- a/app/models/planting.rb +++ b/app/models/planting.rb @@ -83,6 +83,9 @@ class Planting < ApplicationRecord validates :planted_from, allow_blank: true, inclusion: { in: PLANTED_FROM_VALUES, message: "%s is not a valid planting method" } + validates :overall_rating, allow_blank: true, numericality: { + only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 5 + } def planting_slug [ diff --git a/app/views/harvests/_planting.haml b/app/views/harvests/_planting.haml index 5e1eaafe8..cb11a9baa 100644 --- a/app/views/harvests/_planting.haml +++ b/app/views/harvests/_planting.haml @@ -5,3 +5,8 @@ - @matching_plantings.each do |planting| = f.radio_button :planting_id, planting.id, label: planting = f.submit "save", class: 'btn btn-sm' + +- if @harvest.planting.present? && @harvest.planting.overall_rating.blank? + .alert.alert-info{role: "alert"} + This harvest is from a planting that hasn't been rated yet. + = link_to "Rate this planting", edit_planting_path(@harvest.planting), class: 'alert-link' diff --git a/app/views/plantings/_facts.haml b/app/views/plantings/_facts.haml index 5cacccdd0..b4521d5b9 100644 --- a/app/views/plantings/_facts.haml +++ b/app/views/plantings/_facts.haml @@ -89,3 +89,9 @@ - if planting.finished_at.present? %span.plantingfact--finish = planting.finished_at.year + - if planting.overall_rating.present? + .card.fact-card + .card-body + %h5.card-title Overall Rating + %p.card-text + %strong= "#{planting.overall_rating}/5" diff --git a/app/views/plantings/_form.html.haml b/app/views/plantings/_form.html.haml index 0354d484f..f0c40f4e1 100644 --- a/app/views/plantings/_form.html.haml +++ b/app/views/plantings/_form.html.haml @@ -43,6 +43,15 @@ = f.select(:sunniness, Planting::SUNNINESS_VALUES, { include_blank: '', label: 'Sun or shade?' } ) .col-md-4 = f.number_field :quantity, label: 'How many?', min: 1 + .col-md-12 + = f.range_field :overall_rating, min: 1, max: 5, include_blank: 'Leave blank', label: 'Overall Rating', list: "rating-list", title: "How well is the planting going?" + %datalist{"id": "rating-list"} + %option{"value": "1"} Poor + %option{"value": "2"} + %option{"value": "3"} + %option{"value": "4"} + %option{"value": "5"} Great + = f.text_area :description, rows: 6, label: 'Tell us more about it' .row diff --git a/app/views/plantings/show.html.haml b/app/views/plantings/show.html.haml index db5af66c6..9841b8329 100644 --- a/app/views/plantings/show.html.haml +++ b/app/views/plantings/show.html.haml @@ -7,6 +7,15 @@ = tag("meta", property: "og:type", content: "website") = tag("meta", property: "og:url", content: request.original_url) = tag("meta", property: "og:site_name", content: ENV['GROWSTUFF_SITE_NAME']) + - if @planting.overall_rating.present? + %script{type: "application/ld+json"} + :plain + { + "@context": "http://schema.org", + "@type": "Rating", + "ratingValue": "#{@planting.overall_rating}", + "bestRating": "5" + } - content_for :breadcrumbs do %li.breadcrumb-item= link_to 'Plantings', plantings_path diff --git a/db/migrate/20250901110545_add_indexes_crops.rb b/db/migrate/20250901110545_add_indexes_crops.rb index 80d6b522d..0a519bb11 100644 --- a/db/migrate/20250901110545_add_indexes_crops.rb +++ b/db/migrate/20250901110545_add_indexes_crops.rb @@ -4,7 +4,6 @@ class AddIndexesCrops < ActiveRecord::Migration[7.2] add_index :alternate_names, :creator_id add_index :alternate_names, :language - add_index :comments, %i(commentable_type commentable_id) add_index :comments, :author_id add_index :crop_companions, %i(crop_a_id crop_b_id) diff --git a/db/migrate/20250901130830_add_overall_rating_plantings.rb b/db/migrate/20250901130830_add_overall_rating_plantings.rb new file mode 100644 index 000000000..92b487327 --- /dev/null +++ b/db/migrate/20250901130830_add_overall_rating_plantings.rb @@ -0,0 +1,5 @@ +class AddOverallRatingPlantings < ActiveRecord::Migration[7.2] + def change + add_column :plantings, :overall_rating, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 6c279b9a9..b09d3956b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_09_01_110545) do +ActiveRecord::Schema[7.2].define(version: 2025_09_01_130830) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -555,16 +555,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_09_01_110545) do t.index ["slug"], name: "index_plant_parts_on_slug", unique: true end - create_table "planting_problems", force: :cascade do |t| - t.bigint "planting_id" - t.bigint "problem_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["planting_id", "problem_id"], name: "index_planting_problems_on_planting_id_and_problem_id", unique: true - t.index ["planting_id"], name: "index_planting_problems_on_planting_id" - t.index ["problem_id"], name: "index_planting_problems_on_problem_id" - end - create_table "plantings", id: :serial, force: :cascade do |t| t.integer "garden_id", null: false t.integer "crop_id", null: false @@ -586,6 +576,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_09_01_110545) do t.integer "harvests_count", default: 0 t.integer "likes_count", default: 0 t.boolean "failed", default: false, null: false + t.integer "overall_rating" t.index ["crop_id"], name: "index_plantings_on_crop_id" t.index ["garden_id"], name: "index_plantings_on_garden_id" t.index ["owner_id"], name: "index_plantings_on_owner_id" @@ -608,32 +599,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_09_01_110545) do t.index ["slug"], name: "index_posts_on_slug", unique: true end - create_table "problem_posts", force: :cascade do |t| - t.bigint "problem_id" - t.bigint "post_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["post_id"], name: "index_problem_posts_on_post_id" - t.index ["problem_id", "post_id"], name: "index_problem_posts_on_problem_id_and_post_id", unique: true - t.index ["problem_id"], name: "index_problem_posts_on_problem_id" - end - - create_table "problems", force: :cascade do |t| - t.string "name" - t.string "reason_for_rejection" - t.string "rejection_notes" - t.string "approval_status", default: "pending", null: false - t.bigint "requester_id" - t.bigint "creator_id" - t.string "slug" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["creator_id"], name: "index_problems_on_creator_id" - t.index ["name"], name: "index_problems_on_name" - t.index ["requester_id"], name: "index_problems_on_requester_id" - t.index ["slug"], name: "index_problems_on_slug" - end - create_table "roles", id: :serial, force: :cascade do |t| t.string "name", null: false t.text "description" @@ -692,12 +657,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_09_01_110545) do add_foreign_key "mailboxer_receipts", "mailboxer_notifications", column: "notification_id", name: "receipts_on_notification_id" add_foreign_key "photo_associations", "crops" add_foreign_key "photo_associations", "photos" - add_foreign_key "planting_problems", "plantings" - add_foreign_key "planting_problems", "problems" add_foreign_key "plantings", "seeds", column: "parent_seed_id", name: "parent_seed", on_delete: :nullify - add_foreign_key "problem_posts", "posts" - add_foreign_key "problem_posts", "problems" - add_foreign_key "problems", "members", column: "creator_id" - add_foreign_key "problems", "members", column: "requester_id" add_foreign_key "seeds", "plantings", column: "parent_planting_id", name: "parent_planting", on_delete: :nullify end diff --git a/spec/features/plantings/planting_a_crop_spec.rb b/spec/features/plantings/planting_a_crop_spec.rb index 28fc3981d..4aa8a243b 100644 --- a/spec/features/plantings/planting_a_crop_spec.rb +++ b/spec/features/plantings/planting_a_crop_spec.rb @@ -198,6 +198,7 @@ describe "Planting a crop", :js, :search do within "form#new_planting" do fill_in "When?", with: "2014-07-01" check "Mark as finished" + find_by_id('planting_overall_rating').set 4 fill_in "Finished date", with: "2014-08-30" uncheck 'Mark as finished' end @@ -220,6 +221,7 @@ describe "Planting a crop", :js, :search do expect(page).to have_content "planting was successfully created" expect(page).to have_content "Finished" expect(page).to have_content "Aug 2014" + expect(page).to have_content "4/5" # ensure we've indexed in elastic search planting.reindex(refresh: true) From ada567dcab71ea72b3cced980df0991348255862 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 1 Sep 2025 14:28:24 +0000 Subject: [PATCH 008/387] Remove JS testing from footer --- spec/features/footer_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/footer_spec.rb b/spec/features/footer_spec.rb index 147c68a6b..24960f138 100644 --- a/spec/features/footer_spec.rb +++ b/spec/features/footer_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe "footer", :js do +describe "footer" do before { visit root_path } it "footer is on home page" do From 7106b141d9b846df8ef98c3378e06a6502a3786d Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 2 Sep 2025 00:02:17 +0930 Subject: [PATCH 009/387] Update _facts.haml --- app/views/plantings/_facts.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/plantings/_facts.haml b/app/views/plantings/_facts.haml index b4521d5b9..004c03ffb 100644 --- a/app/views/plantings/_facts.haml +++ b/app/views/plantings/_facts.haml @@ -92,6 +92,6 @@ - if planting.overall_rating.present? .card.fact-card .card-body - %h5.card-title Overall Rating + %h3 Overall Rating %p.card-text %strong= "#{planting.overall_rating}/5" From 2f290efc5b56213fadb6be67d933b363d5d0a31a Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 2 Sep 2025 00:03:04 +0930 Subject: [PATCH 010/387] Rename _facts.haml to _facts.html.haml --- app/views/plantings/{_facts.haml => _facts.html.haml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/views/plantings/{_facts.haml => _facts.html.haml} (100%) diff --git a/app/views/plantings/_facts.haml b/app/views/plantings/_facts.html.haml similarity index 100% rename from app/views/plantings/_facts.haml rename to app/views/plantings/_facts.html.haml From 9b9de0614055a8866d0aedbe843c0be1e7476347 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 2 Sep 2025 00:12:59 +0930 Subject: [PATCH 011/387] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c6aec2da6..f13434473 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ encourage participation from people of all backgrounds and skill levels. ## Want to contribute? Don't ask to ask, the best way to get started is to fork the project, start a codespace and get hacking. -Dive on in and submit your PRs. +Dive on in and submit your PRs! + +Vibe Coding is more than okay, just make sure you indicate if you have done so and ensure there are tests. ## Important links From 2b818e9f50023215672b115e11ea2d51195d6405 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 2 Sep 2025 00:33:12 +0930 Subject: [PATCH 012/387] Update README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index c6aec2da6..aa1a3a74b 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ frontend features. We welcome contributions -- see * To set up your development environment, see [Getting started](https://github.com/Growstuff/growstuff/wiki/New-contributor-guide). * You may also be interested in our [API](https://github.com/Growstuff/growstuff/wiki/API). +### For Home Automation enthusiasts + +https://github.com/growstuff/growstuff-homeassistant + ## For designers, writers, researchers, data wranglers, and other contributors There are heaps of ways to get involved and contribute no matter what From ac2d99871109402c4815b7c88147ae455aaa716e Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 2 Sep 2025 00:36:30 +0930 Subject: [PATCH 013/387] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa1a3a74b..c88b728f6 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ frontend features. We welcome contributions -- see ### For Home Automation enthusiasts -https://github.com/growstuff/growstuff-homeassistant +https://github.com/Growstuff/homeassistant-growstuff/ ## For designers, writers, researchers, data wranglers, and other contributors From b3ba05d8340162bb8c19e912efba2aa367960d62 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 2 Sep 2025 02:17:28 +0930 Subject: [PATCH 014/387] Fix crash on adding Flickr photo (#4198) * Update photo.rb * Update photo.rb * Update app/models/photo.rb * Update app/models/photo.rb --- app/models/photo.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/photo.rb b/app/models/photo.rb index 183fadceb..afc65875d 100644 --- a/app/models/photo.rb +++ b/app/models/photo.rb @@ -46,7 +46,8 @@ class Photo < ApplicationRecord flickr = owner.flickr info = flickr.photos.getInfo(photo_id: source_id) licenses = flickr.photos.licenses.getInfo - license = licenses.find { |l| l.id == info.license } + license = licenses.find { |l| l.id.to_i == info.license.to_i } + Rails.logger.error("Cannot find license: " + [info.license, licenses].inspect) unless license { title: calculate_title(info), license_name: license.name, From 765fab110401299e2f3efa7b1aaec3781a4b4d88 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:30:02 +0000 Subject: [PATCH 015/387] CI: Preserve screenshots as build artifacts Adjust the behaviour of capybara-screenshot / GitHub CI to preserve the screenshots as a build artifact. This change adds a step to the `ci-features.yml` workflow to upload the `tmp/screenshots` directory as a build artifact on failure. This will help with debugging failing feature tests. --- .github/workflows/ci-features.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-features.yml b/.github/workflows/ci-features.yml index 4f0e1ad2f..c2d3cc2ba 100644 --- a/.github/workflows/ci-features.yml +++ b/.github/workflows/ci-features.yml @@ -108,4 +108,11 @@ jobs: run: bundle exec rspec spec/features/photos/ -fd - name: Run rspec (rss/) - run: bundle exec rspec spec/features/rss/ -fd \ No newline at end of file + run: bundle exec rspec spec/features/rss/ -fd + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots \ No newline at end of file From d61227bad0e6dcda3654aa9715c909e6d8df2fe8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:30:25 +0000 Subject: [PATCH 016/387] CI: Preserve screenshots as build artifacts Adjust the behaviour of capybara-screenshot / GitHub CI to preserve the screenshots as a build artifact. This change adds a step to all `ci-features-*.yml` workflows to upload the `tmp/screenshots` directory as a build artifact on failure. This will help with debugging failing feature tests. --- .github/workflows/ci-features-admin.yml | 7 +++++++ .github/workflows/ci-features-comments.yml | 7 +++++++ .github/workflows/ci-features-conversations.yml | 6 ++++++ .github/workflows/ci-features-crops.yml | 7 +++++++ .github/workflows/ci-features-gardens.yml | 7 +++++++ .github/workflows/ci-features-harvests.yml | 9 ++++++++- .github/workflows/ci-features-home.yml | 9 ++++++++- .github/workflows/ci-features-members.yml | 9 ++++++++- .github/workflows/ci-features-places.yml | 7 +++++++ .github/workflows/ci-features-plantings.yml | 7 +++++++ .github/workflows/ci-features-posts.yml | 7 +++++++ .github/workflows/ci-features-seeds.yml | 7 +++++++ .github/workflows/ci-features-timeline.yml | 9 ++++++++- 13 files changed, 94 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-features-admin.yml b/.github/workflows/ci-features-admin.yml index 870e6942c..ff491a422 100644 --- a/.github/workflows/ci-features-admin.yml +++ b/.github/workflows/ci-features-admin.yml @@ -100,3 +100,10 @@ jobs: - name: Run rspec (admin/) run: bundle exec rspec spec/features/admin/ -fd -t ~@flaky + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots diff --git a/.github/workflows/ci-features-comments.yml b/.github/workflows/ci-features-comments.yml index aa14b2078..398fc30e5 100644 --- a/.github/workflows/ci-features-comments.yml +++ b/.github/workflows/ci-features-comments.yml @@ -100,3 +100,10 @@ jobs: - name: Run rspec (comments/) run: bundle exec rspec spec/features/comments/ -fd -t ~@flaky + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots diff --git a/.github/workflows/ci-features-conversations.yml b/.github/workflows/ci-features-conversations.yml index 1548c4ebb..c1fa33b66 100644 --- a/.github/workflows/ci-features-conversations.yml +++ b/.github/workflows/ci-features-conversations.yml @@ -101,3 +101,9 @@ jobs: - name: Run rspec (conversations/) run: bundle exec rspec spec/features/conversations/ -fd -t ~@flaky + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots diff --git a/.github/workflows/ci-features-crops.yml b/.github/workflows/ci-features-crops.yml index 182cbe9b6..4a999de2a 100644 --- a/.github/workflows/ci-features-crops.yml +++ b/.github/workflows/ci-features-crops.yml @@ -100,3 +100,10 @@ jobs: - name: Run rspec (crops/) run: bundle exec rspec spec/features/crops/ -fd -t ~@flaky + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots diff --git a/.github/workflows/ci-features-gardens.yml b/.github/workflows/ci-features-gardens.yml index cc96d742e..f64a8d36a 100644 --- a/.github/workflows/ci-features-gardens.yml +++ b/.github/workflows/ci-features-gardens.yml @@ -100,3 +100,10 @@ jobs: - name: Run rspec (gardens/) run: bundle exec rspec spec/features/gardens/ -fd -t ~@flaky + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots diff --git a/.github/workflows/ci-features-harvests.yml b/.github/workflows/ci-features-harvests.yml index c3047c533..41a29f859 100644 --- a/.github/workflows/ci-features-harvests.yml +++ b/.github/workflows/ci-features-harvests.yml @@ -99,4 +99,11 @@ jobs: run: bundle exec rails search:reindex - name: Run rspec (harvests/) - run: bundle exec rspec spec/features/harvests/ -fd -t ~@flaky \ No newline at end of file + run: bundle exec rspec spec/features/harvests/ -fd -t ~@flaky + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots \ No newline at end of file diff --git a/.github/workflows/ci-features-home.yml b/.github/workflows/ci-features-home.yml index 43c72a922..1676ca10e 100644 --- a/.github/workflows/ci-features-home.yml +++ b/.github/workflows/ci-features-home.yml @@ -99,4 +99,11 @@ jobs: run: bundle exec rails search:reindex - name: Run rspec (home/) - run: bundle exec rspec spec/features/home/ -fd -t ~@flaky \ No newline at end of file + run: bundle exec rspec spec/features/home/ -fd -t ~@flaky + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots \ No newline at end of file diff --git a/.github/workflows/ci-features-members.yml b/.github/workflows/ci-features-members.yml index c542c93e9..8bce06f19 100644 --- a/.github/workflows/ci-features-members.yml +++ b/.github/workflows/ci-features-members.yml @@ -99,4 +99,11 @@ jobs: run: bundle exec rails search:reindex - name: Run rspec (members/) - run: bundle exec rspec spec/features/members/ -fd -t ~@flaky \ No newline at end of file + run: bundle exec rspec spec/features/members/ -fd -t ~@flaky + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots \ No newline at end of file diff --git a/.github/workflows/ci-features-places.yml b/.github/workflows/ci-features-places.yml index 6a4cd4256..ebb175946 100644 --- a/.github/workflows/ci-features-places.yml +++ b/.github/workflows/ci-features-places.yml @@ -100,3 +100,10 @@ jobs: - name: Run rspec (places/) run: bundle exec rspec spec/features/places/ -fd + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots diff --git a/.github/workflows/ci-features-plantings.yml b/.github/workflows/ci-features-plantings.yml index 6a7e50146..fdfc7b0b9 100644 --- a/.github/workflows/ci-features-plantings.yml +++ b/.github/workflows/ci-features-plantings.yml @@ -100,3 +100,10 @@ jobs: - name: Run rspec (plantings/) run: bundle exec rspec spec/features/plantings/ -fd + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots diff --git a/.github/workflows/ci-features-posts.yml b/.github/workflows/ci-features-posts.yml index 1353c891f..9a7b7e78b 100644 --- a/.github/workflows/ci-features-posts.yml +++ b/.github/workflows/ci-features-posts.yml @@ -100,3 +100,10 @@ jobs: - name: Run rspec (posts/) run: bundle exec rspec spec/features/posts/ -fd + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots diff --git a/.github/workflows/ci-features-seeds.yml b/.github/workflows/ci-features-seeds.yml index 41bbecbf2..d38390c95 100644 --- a/.github/workflows/ci-features-seeds.yml +++ b/.github/workflows/ci-features-seeds.yml @@ -100,3 +100,10 @@ jobs: - name: Run rspec (seeds/) run: bundle exec rspec spec/features/seeds/ -fd -t ~@flaky + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots diff --git a/.github/workflows/ci-features-timeline.yml b/.github/workflows/ci-features-timeline.yml index 3560dbd81..a5c8c830b 100644 --- a/.github/workflows/ci-features-timeline.yml +++ b/.github/workflows/ci-features-timeline.yml @@ -99,4 +99,11 @@ jobs: run: bundle exec rails search:reindex - name: Run rspec (timeline/) - run: bundle exec rspec spec/features/timeline/ -fd -t ~@flaky \ No newline at end of file + run: bundle exec rspec spec/features/timeline/ -fd -t ~@flaky + + - name: Upload screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tmp/screenshots \ No newline at end of file From dc1b46c7becbeb1655e8e7d54d6004392a17013a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 05:55:09 +0000 Subject: [PATCH 017/387] Bump rubocop-rspec from 3.6.0 to 3.7.0 Bumps [rubocop-rspec](https://github.com/rubocop/rubocop-rspec) from 3.6.0 to 3.7.0. - [Release notes](https://github.com/rubocop/rubocop-rspec/releases) - [Changelog](https://github.com/rubocop/rubocop-rspec/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop/rubocop-rspec/compare/v3.6.0...v3.7.0) --- updated-dependencies: - dependency-name: rubocop-rspec dependency-version: 3.7.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2509c7f34..02115c812 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -630,7 +630,7 @@ GEM rubocop-rake (0.7.1) lint_roller (~> 1.1) rubocop (>= 1.72.1) - rubocop-rspec (3.6.0) + rubocop-rspec (3.7.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) rubocop-rspec_rails (2.31.0) From 1dc587d4b5fcbc12cfe87e42814b7b6a7ebb9849 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 00:26:26 +0000 Subject: [PATCH 018/387] Bump pg from 1.6.1 to 1.6.2 Bumps [pg](https://github.com/ged/ruby-pg) from 1.6.1 to 1.6.2. - [Changelog](https://github.com/ged/ruby-pg/blob/master/CHANGELOG.md) - [Commits](https://github.com/ged/ruby-pg/compare/v1.6.1...v1.6.2) --- updated-dependencies: - dependency-name: pg dependency-version: 1.6.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2509c7f34..5ce117c55 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -457,8 +457,8 @@ GEM racc percy-capybara (5.0.0) capybara (>= 3) - pg (1.6.1) - pg (1.6.1-x86_64-linux) + pg (1.6.2) + pg (1.6.2-x86_64-linux) platform-api (3.8.0) heroics (~> 0.1.1) moneta (~> 1.0.0) From b0b759ef60154896979f53499f818564a086935c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 07:59:48 +0000 Subject: [PATCH 019/387] Bump puma from 6.6.1 to 7.0.0 Bumps [puma](https://github.com/puma/puma) from 6.6.1 to 7.0.0. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v6.6.1...v7.0.0) --- updated-dependencies: - dependency-name: puma dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 544c6c183..b15f5835d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -475,7 +475,7 @@ GEM date stringio public_suffix (6.0.1) - puma (6.6.1) + puma (7.0.0) nio4r (~> 2.0) query_diet (0.7.2) racc (1.8.1) From 30f799c4b9a941d669068ff941417ef571e31002 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 08:10:40 +0000 Subject: [PATCH 020/387] Bump rubocop from 1.80.1 to 1.80.2 Bumps [rubocop](https://github.com/rubocop/rubocop) from 1.80.1 to 1.80.2. - [Release notes](https://github.com/rubocop/rubocop/releases) - [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md) - [Commits](https://github.com/rubocop/rubocop/compare/v1.80.1...v1.80.2) --- updated-dependencies: - dependency-name: rubocop dependency-version: 1.80.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index b15f5835d..6247f9810 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -601,7 +601,7 @@ GEM rswag-ui (2.16.0) actionpack (>= 5.2, < 8.1) railties (>= 5.2, < 8.1) - rubocop (1.80.1) + rubocop (1.80.2) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) From f650d1b8fa1eed651ca65dde0601bcfb2af045a3 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 03:54:20 +0000 Subject: [PATCH 021/387] Change garden sort order to be by name and planting by most to lease recent when creating an activity --- app/views/activities/_form.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/activities/_form.html.haml b/app/views/activities/_form.html.haml index 879117f8b..8ff9fae2e 100644 --- a/app/views/activities/_form.html.haml +++ b/app/views/activities/_form.html.haml @@ -27,13 +27,13 @@ .row .col-md-4 - = f.collection_radio_buttons(:garden_id, @activity.owner.gardens.active, + = f.collection_radio_buttons(:garden_id, @activity.owner.gardens.active.order_by_name, :id, :name, label: 'Is this for a specific garden?') = link_to "Add a garden.", new_garden_path .col-md-4 - = f.collection_radio_buttons(:planting_id, @activity.owner.plantings.active, + = f.collection_radio_buttons(:planting_id, @activity.owner.plantings.active.recent, :id, :crop_name, label: 'Is this for a specific planting?') = link_to "Add a planting.", new_planting_path From 84da4c0f4fe2a85b49c23a03ee38be115307c2c1 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 04:24:22 +0000 Subject: [PATCH 022/387] Fix styling of cards to space evenly --- app/assets/stylesheets/overrides.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/overrides.scss b/app/assets/stylesheets/overrides.scss index 670269aa8..c21aad2df 100755 --- a/app/assets/stylesheets/overrides.scss +++ b/app/assets/stylesheets/overrides.scss @@ -132,6 +132,8 @@ section { border-radius: 5%; margin: 0.5em 0.5em 0.5em 0; width: 200px; + align-items: stretch; + justify-content: space-between; .img-card { border-top-left-radius: 5%; From d8b84e611b8d18755b62abbc86cfa40ae9d55f72 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 14:37:18 +0930 Subject: [PATCH 023/387] Update ci-features-posts.yml --- .github/workflows/ci-features-posts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-features-posts.yml b/.github/workflows/ci-features-posts.yml index 9a7b7e78b..9b973163a 100644 --- a/.github/workflows/ci-features-posts.yml +++ b/.github/workflows/ci-features-posts.yml @@ -1,4 +1,4 @@ -name: CI Features - Admin +name: CI Features - Posts on: [pull_request] From 9b1699b06121b3b3c1332bff37cce6734cd519d6 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 14:39:13 +0930 Subject: [PATCH 024/387] Merge pull request #4215 from Growstuff/activities-detail Surface more Activities detail --- app/helpers/application_helper.rb | 22 ++++++++++++++++++ app/models/concerns/search_activities.rb | 6 ++++- app/views/activities/_card.html.haml | 29 +++++++++++++++--------- app/views/crops/_harvests.html.haml | 2 +- app/views/harvests/show.html.haml | 2 +- 5 files changed, 47 insertions(+), 14 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 98fb076be..526beadc1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -21,6 +21,28 @@ module ApplicationHelper classes end + # Similar to Rails' time_ago_in_words, but gives a more standard + # output like "in 3 days" or "5 months ago". + # Also handles the case where from_time is a Date and to_time is a Date + # (in which case it just says "today" if they're the same date). + # + # NOTE: This is similar to distance_of_time_in_words but different enough + # that I think it's worth having a separate helper for it. + # + # from_time - the starting time (Time or Date) + # to_time - the ending time (Time or Date). Default: now (Time.zone.now) + # include_seconds - whether to include seconds in the calculation + # + # Returns a string like "in 3 days" or "5 months ago" + def standard_time_distance(from_time, to_time = 0, include_seconds = false) + return 'today' if from_time.is_a?(Date) && (from_time == to_time) + + return 'now' if from_time == to_time + return distance_of_time_in_words(from_time, to_time, include_seconds:) + ' ago' if from_time > to_time + + 'in ' + distance_of_time_in_words(from_time, to_time, include_seconds:) + end + def count_github_contibutors File.open(Rails.root.join('CONTRIBUTORS.md')).readlines.grep(/^-/).size end diff --git a/app/models/concerns/search_activities.rb b/app/models/concerns/search_activities.rb index 458d0921c..8190b4a7c 100644 --- a/app/models/concerns/search_activities.rb +++ b/app/models/concerns/search_activities.rb @@ -9,7 +9,9 @@ module SearchActivities mappings: { properties: { active: { type: :boolean }, - created_at: { type: :integer } + created_at: { type: :integer }, + updated_at: { type: :integer }, + due_date: { type: :date } } } @@ -23,8 +25,10 @@ module SearchActivities category:, garden_id:, garden_name: garden&.name, + garden_slug: garden&.garden_slug, planting_id:, planting_name: planting&.crop&.name, + planting_slug: planting&.slug, description:, # owner diff --git a/app/views/activities/_card.html.haml b/app/views/activities/_card.html.haml index eafef62f4..ca0a17909 100644 --- a/app/views/activities/_card.html.haml +++ b/app/views/activities/_card.html.haml @@ -20,16 +20,23 @@ - if can? :destroy, activity .dropdown-divider = delete_button(activity, classes: 'dropdown-item text-danger') - = link_to activity_path(slug: activity.slug) do - .card-body.text-center + .card-body + = link_to activity_path(slug: activity.slug) do %h4= activity.name - .text-center= activity.description - - if activity.garden - .text-center= activity.garden - - if activity.planting - .text-center= activity.planting + %small.due-date{title: activity.due_date} + = standard_time_distance(activity.due_date.to_date, Time.zone.now.to_date) + %div + %small.text-justify{title: activity.description}= activity.description.truncate(150) + %p + %ul.list-unstyled + - if activity.garden_name && activity.garden_slug + %li + %small= link_to activity.garden_name, garden_path(slug: activity.garden_slug) + - if activity.planting_name && activity.planting_slug + %li + %small= link_to activity.planting_name, planting_path(slug: activity.planting_slug) + .card-footer - .float-right - %span.chip.member-chip - = link_to member_path(slug: activity.owner_slug) do - = activity.owner_login_name + %small.chip.member-chip + = link_to member_path(slug: activity.owner_slug) do + = activity.owner_login_name diff --git a/app/views/crops/_harvests.html.haml b/app/views/crops/_harvests.html.haml index f2755bbc6..47f43d57a 100644 --- a/app/views/crops/_harvests.html.haml +++ b/app/views/crops/_harvests.html.haml @@ -12,7 +12,7 @@ #{harvest.owner} harvested #{display_quantity(harvest)}. .float-right= render 'members/location', member: harvest.owner .harvest-timeago - %small #{distance_of_time_in_words(harvest.harvested_at, Time.zone.now)} ago. + %small #{standard_time_distance(harvest.harvested_at, Time.zone.now.to_date)} %li.list-group-item= link_to "View all #{crop.name} harvests", crop_harvests_path(crop), class: 'card-link' - if crop.approved? - if current_member diff --git a/app/views/harvests/show.html.haml b/app/views/harvests/show.html.haml index 88d55664a..37404368f 100644 --- a/app/views/harvests/show.html.haml +++ b/app/views/harvests/show.html.haml @@ -46,7 +46,7 @@ %h3 Harvested = editable :date, @harvest, :harvested_at, display_field: '.harvested_at' - %strong.harvested_at #{distance_of_time_in_words @harvest.harvested_at, Time.zone.now.to_date} ago + %strong.harvested_at #{standard_time_distance @harvest.harvested_at, Time.zone.now.to_date} %span.harvested_at= I18n.l @harvest.harvested_at .card{class: @harvest.quantity.present? ? '' : 'text-muted'} From 1eb84b97653d352e11e55dd1a2a13fff55b604c2 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 05:11:30 +0000 Subject: [PATCH 025/387] By default, skip to content when linking to garden(s) --- app/views/home/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 6e8c60d9a..af03adf2a 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -15,7 +15,7 @@ = planting_icon Track my plantings %p - = link_to member_gardens_path(current_member), class: 'btn btn-dark' do + = link_to member_gardens_path(current_member, anchor: "#content"), class: 'btn btn-dark' do = garden_icon Show me my garden - else From b3c749566bf2d6c3e0e430ee2049654b3577075e Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 05:14:18 +0000 Subject: [PATCH 026/387] More named anchors --- app/views/home/index.html.haml | 4 ++-- app/views/members/show.html.haml | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index af03adf2a..7b63ee8f3 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -11,11 +11,11 @@ %br %p - if current_member.plantings.active.any? - = link_to member_path(current_member, anchor: "#content"), class: 'btn btn-dark' do + = link_to member_path(current_member, anchor: "content"), class: 'btn btn-dark' do = planting_icon Track my plantings %p - = link_to member_gardens_path(current_member, anchor: "#content"), class: 'btn btn-dark' do + = link_to member_gardens_path(current_member, anchor: "content"), class: 'btn btn-dark' do = garden_icon Show me my garden - else diff --git a/app/views/members/show.html.haml b/app/views/members/show.html.haml index d4654b8f8..4779bc88e 100644 --- a/app/views/members/show.html.haml +++ b/app/views/members/show.html.haml @@ -83,14 +83,14 @@ .row %section.order-3.order-md-1.col-12= render "map", member: @member - if @harvesting.size.positive? - %section.harvests.order-2.order-md-1.col-12 + %section.harvests.order-2.order-md-1.col-12#harvests %h2 Ready to harvest .index-cards - @harvesting.each do |planting| = render 'plantings/thumbnail', planting: planting - if @others.size.positive? - %section.planting-progress.order-2.order-md-1.col-12 + %section.planting-progress.order-2.order-md-1.col-12#planting-progress %h2 Progress report %p Still growing and not ready for harvesting. .list-group @@ -99,7 +99,7 @@ %span= render 'plantings/tiny', planting: planting %span= render 'plantings/progress', planting: planting - if @late.size.positive? - %section.late.order-2.order-md-1.col-12 + %section.late.order-2.order-md-1.col-12#late %h2 Late %p These plantings are at the end of their lifecycle. @@ -109,7 +109,7 @@ - @late.each do |planting| = render 'plantings/thumbnail', planting: planting - if @super_late.any? - %section.superlate.order-2.order-md-1.col-12 + %section.superlate.order-2.order-md-1.col-12#superlate %h2 Super late %p We suspect the following plantings finished long ago and no longer need tracking. @@ -122,14 +122,14 @@ planted on #{planting.planted_at.to_date} - if @harvests.any? - %section.havests.order-2.order-md-1.col-12 + %section.havests.order-2.order-md-1.col-12#recent-harvests %h2 Recent Harvests .index-cards - @harvests.each do |harvest| = render 'harvests/thumbnail', harvest: harvest - if @activity.any? - %section.activity.order-2.order-md-1.col-12 + %section.activity.order-2.order-md-1.col-12#activity %h2 Activity .list-group - @activity.each do |event| From 186f07109c6203be32cb6ad8bd85871d46666adb Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 05:17:14 +0000 Subject: [PATCH 027/387] Add skip link --- app/views/gardens/index.html.haml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/gardens/index.html.haml b/app/views/gardens/index.html.haml index 277990856..3930cdf71 100644 --- a/app/views/gardens/index.html.haml +++ b/app/views/gardens/index.html.haml @@ -11,6 +11,9 @@ .row .col-md-2 + %small + %a{href: "#content"} + Skip to main content = render 'layouts/nav', model: Garden %label = link_to show_inactive_tickbox_path('gardens', owner: @owner, show_all: @show_all) do @@ -20,7 +23,7 @@ %hr/ = render @owner - .col-md-10 + .col-md-10#content - if @gardens.empty? %p There are no gardens to display. - if can?(:create, Garden) && @owner == current_member From b2d85309230782b744585024636c175d8b0167aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 05:19:53 +0000 Subject: [PATCH 028/387] Bump actions/setup-node from 4 to 5 Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci-features-admin.yml | 2 +- .github/workflows/ci-features-comments.yml | 2 +- .github/workflows/ci-features-conversations.yml | 2 +- .github/workflows/ci-features-crops.yml | 2 +- .github/workflows/ci-features-gardens.yml | 2 +- .github/workflows/ci-features-harvests.yml | 2 +- .github/workflows/ci-features-home.yml | 2 +- .github/workflows/ci-features-members.yml | 2 +- .github/workflows/ci-features-places.yml | 2 +- .github/workflows/ci-features-plantings.yml | 2 +- .github/workflows/ci-features-posts.yml | 2 +- .github/workflows/ci-features-seeds.yml | 2 +- .github/workflows/ci-features-timeline.yml | 2 +- .github/workflows/ci-features.yml | 2 +- .github/workflows/ci.yml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci-features-admin.yml b/.github/workflows/ci-features-admin.yml index ff491a422..539bafd8e 100644 --- a/.github/workflows/ci-features-admin.yml +++ b/.github/workflows/ci-features-admin.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-comments.yml b/.github/workflows/ci-features-comments.yml index 398fc30e5..3aba86370 100644 --- a/.github/workflows/ci-features-comments.yml +++ b/.github/workflows/ci-features-comments.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-conversations.yml b/.github/workflows/ci-features-conversations.yml index c1fa33b66..225b9a129 100644 --- a/.github/workflows/ci-features-conversations.yml +++ b/.github/workflows/ci-features-conversations.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-crops.yml b/.github/workflows/ci-features-crops.yml index 4a999de2a..70a69a7da 100644 --- a/.github/workflows/ci-features-crops.yml +++ b/.github/workflows/ci-features-crops.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-gardens.yml b/.github/workflows/ci-features-gardens.yml index f64a8d36a..b48002eb3 100644 --- a/.github/workflows/ci-features-gardens.yml +++ b/.github/workflows/ci-features-gardens.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-harvests.yml b/.github/workflows/ci-features-harvests.yml index 41a29f859..00eedbb9a 100644 --- a/.github/workflows/ci-features-harvests.yml +++ b/.github/workflows/ci-features-harvests.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-home.yml b/.github/workflows/ci-features-home.yml index 1676ca10e..f58358f28 100644 --- a/.github/workflows/ci-features-home.yml +++ b/.github/workflows/ci-features-home.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-members.yml b/.github/workflows/ci-features-members.yml index 8bce06f19..9fa4a307d 100644 --- a/.github/workflows/ci-features-members.yml +++ b/.github/workflows/ci-features-members.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-places.yml b/.github/workflows/ci-features-places.yml index ebb175946..f65b2185b 100644 --- a/.github/workflows/ci-features-places.yml +++ b/.github/workflows/ci-features-places.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-plantings.yml b/.github/workflows/ci-features-plantings.yml index fdfc7b0b9..eafa326c9 100644 --- a/.github/workflows/ci-features-plantings.yml +++ b/.github/workflows/ci-features-plantings.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-posts.yml b/.github/workflows/ci-features-posts.yml index 9b973163a..4d58f546c 100644 --- a/.github/workflows/ci-features-posts.yml +++ b/.github/workflows/ci-features-posts.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-seeds.yml b/.github/workflows/ci-features-seeds.yml index d38390c95..e5d5bdbb3 100644 --- a/.github/workflows/ci-features-seeds.yml +++ b/.github/workflows/ci-features-seeds.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features-timeline.yml b/.github/workflows/ci-features-timeline.yml index a5c8c830b..0ddc0660d 100644 --- a/.github/workflows/ci-features-timeline.yml +++ b/.github/workflows/ci-features-timeline.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci-features.yml b/.github/workflows/ci-features.yml index c2d3cc2ba..4b5afc437 100644 --- a/.github/workflows/ci-features.yml +++ b/.github/workflows/ci-features.yml @@ -74,7 +74,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eaed8103e..f9aa42abf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: sudo apt-get -y install libpq-dev google-chrome-stable - name: Install NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '12' From 8fbc02caf31ff8211feb45affdcd04e71c2a5091 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 05:20:35 +0000 Subject: [PATCH 029/387] Timeline --- app/views/layouts/_menu.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/_menu.haml b/app/views/layouts/_menu.haml index 749db0261..17a70150d 100644 --- a/app/views/layouts/_menu.haml +++ b/app/views/layouts/_menu.haml @@ -2,10 +2,10 @@ %ul.navbar-nav.mr-auto.bg-dark - if signed_in? %li.nav-item - = link_to timeline_index_path, method: :get, class: 'nav-link text-white' do + = link_to timeline_index_path, method: :get, class: 'nav-link text-white', title: "Timeline" do = image_tag 'icons/notification.svg', class: 'img img-icon', alt: "Notifications" %li.nav-item - = link_to member_gardens_path(current_member), class: 'nav-link text-white', title: "My gardens" do + = link_to member_gardens_path(current_member, anchor: "content"), class: 'nav-link text-white', title: "My gardens" do = image_icon 'gardens' %li.nav-item.dropdown %a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"} From 3725957065d9cb75202854f57941945616fd66ca Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 05:34:49 +0000 Subject: [PATCH 030/387] Only render if available --- app/views/activities/_card.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/activities/_card.html.haml b/app/views/activities/_card.html.haml index ca0a17909..372d064c2 100644 --- a/app/views/activities/_card.html.haml +++ b/app/views/activities/_card.html.haml @@ -23,8 +23,9 @@ .card-body = link_to activity_path(slug: activity.slug) do %h4= activity.name - %small.due-date{title: activity.due_date} - = standard_time_distance(activity.due_date.to_date, Time.zone.now.to_date) + - if activity.due_date + %small.due-date{title: activity.due_date} + = standard_time_distance(activity.due_date.to_date, Time.zone.now.to_date) %div %small.text-justify{title: activity.description}= activity.description.truncate(150) %p From 3917f263b8017307525d430831220794d89c2cae Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 17:09:27 +0930 Subject: [PATCH 031/387] Fix garden order on planting new --- app/views/plantings/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/plantings/_form.html.haml b/app/views/plantings/_form.html.haml index f0c40f4e1..c287c4f7c 100644 --- a/app/views/plantings/_form.html.haml +++ b/app/views/plantings/_form.html.haml @@ -27,7 +27,7 @@ .row .col-md-8 - = f.collection_radio_buttons(:garden_id, @planting.owner.gardens.active, + = f.collection_radio_buttons(:garden_id, @planting.owner.gardens.active.order_by_name, :id, :name, required: true, label: 'Where did you plant it?') = link_to "Add a garden.", new_garden_path From b0555ef89ee07e5bbf36975c17b0f7375e9c159d Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 17:12:14 +0930 Subject: [PATCH 032/387] Update _modal.html.haml to sort consistently --- app/views/plantings/_modal.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/plantings/_modal.html.haml b/app/views/plantings/_modal.html.haml index d00c6d3f4..928c480d2 100644 --- a/app/views/plantings/_modal.html.haml +++ b/app/views/plantings/_modal.html.haml @@ -9,7 +9,7 @@ %p Which garden is the planting in? %ul.list-group - - planting.owner.gardens.active.order(:name).each do |garden| + - planting.owner.gardens.active.order_by_name.each do |garden| %li.list-group-item = link_to plantings_path(planting: {crop_id: planting.crop_id, garden_id: garden.id}), method: :post do .md-v-line From 2bc164bd2ed5b3b64af993d991555e5844ffe069 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 07:59:51 +0000 Subject: [PATCH 033/387] Render history --- app/controllers/gardens_controller.rb | 1 + app/controllers/plantings_controller.rb | 1 + app/views/gardens/_previously.haml | 5 +++++ app/views/plantings/show.html.haml | 6 +++++- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/controllers/gardens_controller.rb b/app/controllers/gardens_controller.rb index 7bbf05f7b..3d50dfa03 100644 --- a/app/controllers/gardens_controller.rb +++ b/app/controllers/gardens_controller.rb @@ -20,6 +20,7 @@ class GardensController < DataController def show @current_plantings = @garden.plantings.current.where.not(failed: true).includes(:crop, :owner).order(planted_at: :desc) @current_activities = @garden.activities.current.includes(:owner).order(created_at: :desc) + @finished_activities = @garden.activities.finished.includes(:owner).order(created_at: :desc) @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) diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index 07a73cbce..c123fb869 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -37,6 +37,7 @@ class PlantingsController < DataController @photos = @planting.photos.includes(:owner).order(date_taken: :desc) @harvests = Harvest.search(where: { planting_id: @planting.id }) @current_activities = @planting.activities.current.includes(:owner).order(created_at: :desc) + @finished_activities = @planting.activities.finished.includes(:owner).order(created_at: :desc) @matching_seeds = matching_seeds @crop = @planting.crop diff --git a/app/views/gardens/_previously.haml b/app/views/gardens/_previously.haml index c0c8d9968..8da1fa25b 100644 --- a/app/views/gardens/_previously.haml +++ b/app/views/gardens/_previously.haml @@ -16,3 +16,8 @@ .col-md-12 %p Nothing has been planted here. +- if @finished_activities&.size&.positive? + %h2 Finished activities in garden + .index-cards + - @finished_activities.each do |activity| + = render "activities/card", activity: activity \ No newline at end of file diff --git a/app/views/plantings/show.html.haml b/app/views/plantings/show.html.haml index 9841b8329..21a6d0635 100644 --- a/app/views/plantings/show.html.haml +++ b/app/views/plantings/show.html.haml @@ -89,7 +89,11 @@ - else .col-md-12 %p Nothing is currently planned here. - + - if @finished_activities&.size&.positive? + %h2 Finished activities for planting + .index-cards + - @finished_activities.each do |activity| + = render "activities/card", activity: activity .col-md-4.col-xs-12 = render @planting.crop From d22555ee42466cfd0f37a5e675209f6d0985de75 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 08:04:14 +0000 Subject: [PATCH 034/387] Deep link to content when rating --- app/views/harvests/_planting.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/harvests/_planting.haml b/app/views/harvests/_planting.haml index cb11a9baa..ffbf710a3 100644 --- a/app/views/harvests/_planting.haml +++ b/app/views/harvests/_planting.haml @@ -9,4 +9,4 @@ - if @harvest.planting.present? && @harvest.planting.overall_rating.blank? .alert.alert-info{role: "alert"} This harvest is from a planting that hasn't been rated yet. - = link_to "Rate this planting", edit_planting_path(@harvest.planting), class: 'alert-link' + = link_to "Rate this planting", edit_planting_path(@harvest.planting, anchor: "planting_overall_rating"), class: 'alert-link' From 42036a3d3fdaa5a6c0a3e085266d29b5de0a7f6d Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 08:07:11 +0000 Subject: [PATCH 035/387] Fix logic --- app/helpers/application_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 526beadc1..0597ed275 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -38,7 +38,7 @@ module ApplicationHelper return 'today' if from_time.is_a?(Date) && (from_time == to_time) return 'now' if from_time == to_time - return distance_of_time_in_words(from_time, to_time, include_seconds:) + ' ago' if from_time > to_time + return distance_of_time_in_words(from_time, to_time, include_seconds:) + ' ago' if from_time < to_time 'in ' + distance_of_time_in_words(from_time, to_time, include_seconds:) end From 798eb1132f06e55920d2d680ac1f77fa97c75fe3 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 7 Sep 2025 08:10:41 +0000 Subject: [PATCH 036/387] Rubocop --- app/helpers/application_helper.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0597ed275..6377e9aa2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -38,9 +38,9 @@ module ApplicationHelper return 'today' if from_time.is_a?(Date) && (from_time == to_time) return 'now' if from_time == to_time - return distance_of_time_in_words(from_time, to_time, include_seconds:) + ' ago' if from_time < to_time + return "#{distance_of_time_in_words(from_time, to_time, include_seconds:)} ago" if from_time < to_time - 'in ' + distance_of_time_in_words(from_time, to_time, include_seconds:) + "in #{distance_of_time_in_words(from_time, to_time, include_seconds:)}" end def count_github_contibutors From bc11a1b8db1b6d00edfa049946da69475dabc8e4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 20:03:18 +0930 Subject: [PATCH 037/387] Merge pull request #4209 from Growstuff/extend-crop-model Extend Crop Model and Migrate Data from OpenFarm --- app/controllers/crops_controller.rb | 2 ++ app/models/concerns/open_farm_data.rb | 24 -------------- app/views/crops/_form.html.haml | 8 +++++ app/views/crops/_openfarm_data.html.haml | 33 +++++++++++++++++++ app/views/crops/show.html.haml | 2 ++ .../20240101010101_add_fields_to_crops.rb | 10 ++++++ ...populate_crop_fields_from_openfarm_data.rb | 21 ++++++++++++ db/schema.rb | 6 ++++ spec/controllers/crops_controller_spec.rb | 30 +++++++++++++++++ spec/features/crops/creating_a_crop_spec.rb | 8 +++++ 10 files changed, 120 insertions(+), 24 deletions(-) create mode 100644 app/views/crops/_openfarm_data.html.haml create mode 100644 db/migrate/20240101010101_add_fields_to_crops.rb create mode 100644 db/migrate/20240101010102_populate_crop_fields_from_openfarm_data.rb diff --git a/app/controllers/crops_controller.rb b/app/controllers/crops_controller.rb index 5a3822e64..8a76566d0 100644 --- a/app/controllers/crops_controller.rb +++ b/app/controllers/crops_controller.rb @@ -192,6 +192,8 @@ class CropsController < ApplicationController :parent_id, :perennial, :request_notes, :reason_for_rejection, :rejection_notes, + :row_spacing, :spread, :height, + :sowing_method, :sun_requirements, :growing_degree_days, scientific_names_attributes: %i(scientific_name _destroy id) ) end diff --git a/app/models/concerns/open_farm_data.rb b/app/models/concerns/open_farm_data.rb index 64bcee0b5..891a71c07 100644 --- a/app/models/concerns/open_farm_data.rb +++ b/app/models/concerns/open_farm_data.rb @@ -8,14 +8,6 @@ module OpenFarmData fetch_attr('main_image_path') end - def height - fetch_attr('height') - end - - def spread - fetch_attr('spread') - end - def svg_icon icon = fetch_attr('svg_icon') return icon if icon.present? @@ -31,10 +23,6 @@ module OpenFarmData fetch_attr('description') end - def row_spacing - fetch_attr('row_spacing') - end - def common_names fetch_attr('common_names') end @@ -43,22 +31,10 @@ module OpenFarmData fetch_attr('binomial_name') end - def sowing_method - fetch_attr('sowing_method') - end - def main_image_path fetch_attr('main_image_path') end - def sun_requirements - fetch_attr('sun_requirements') - end - - def growing_degree_days - fetch_attr('growing_degree_days') - end - def processing_pictures fetch_attr('processing_pictures') end diff --git a/app/views/crops/_form.html.haml b/app/views/crops/_form.html.haml index 18d082172..8797bfe81 100644 --- a/app/views/crops/_form.html.haml +++ b/app/views/crops/_form.html.haml @@ -41,6 +41,14 @@ = f.radio_button(:perennial, true, label: "Perennial") %span.help-block Living more than two years + %h2 OpenFarm Data + = f.number_field :row_spacing, label: 'Row Spacing (cm)', min: 0 + = f.number_field :spread, label: 'Spread (cm)', min: 0 + = f.number_field :height, label: 'Height (cm)', min: 0 + = f.text_field :sowing_method + = f.text_field :sun_requirements + = f.number_field :growing_degree_days, min: 0 + - unless @crop.approved? = link_to 'Search wikipedia', "https://en.wikipedia.org/w/index.php?search=#{@crop.name}", target: '_blank' = f.url_field :en_wikipedia_url, id: "en_wikipedia_url", label: 'Wikipedia URL' diff --git a/app/views/crops/_openfarm_data.html.haml b/app/views/crops/_openfarm_data.html.haml new file mode 100644 index 000000000..900a47c90 --- /dev/null +++ b/app/views/crops/_openfarm_data.html.haml @@ -0,0 +1,33 @@ +- if crop.row_spacing || crop.spread || crop.height || crop.sowing_method || crop.sun_requirements || crop.growing_degree_days + = cute_icon + .card + .card-body + %h4 OpenFarm Data + %ul.list-group.list-group-flush + - if crop.row_spacing + %li.list-group-item + %strong Row Spacing: + = crop.row_spacing + cm + - if crop.spread + %li.list-group-item + %strong Spread: + = crop.spread + cm + - if crop.height + %li.list-group-item + %strong Height: + = crop.height + cm + - if crop.sowing_method + %li.list-group-item + %strong Sowing Method: + = crop.sowing_method + - if crop.sun_requirements + %li.list-group-item + %strong Sun Requirements: + = crop.sun_requirements + - if crop.growing_degree_days + %li.list-group-item + %strong Growing Degree Days: + = crop.growing_degree_days diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index 8478b09d4..25edfc4c8 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -111,6 +111,8 @@ = render 'harvests', crop: @crop = render 'find_seeds', crop: @crop + = render 'openfarm_data', crop: @crop + = cute_icon .card .card-body diff --git a/db/migrate/20240101010101_add_fields_to_crops.rb b/db/migrate/20240101010101_add_fields_to_crops.rb new file mode 100644 index 000000000..59cbcda90 --- /dev/null +++ b/db/migrate/20240101010101_add_fields_to_crops.rb @@ -0,0 +1,10 @@ +class AddFieldsToCrops < ActiveRecord::Migration[5.2] + def change + add_column :crops, :row_spacing, :integer + add_column :crops, :spread, :integer + add_column :crops, :height, :integer + add_column :crops, :sowing_method, :string + add_column :crops, :sun_requirements, :string + add_column :crops, :growing_degree_days, :integer + end +end diff --git a/db/migrate/20240101010102_populate_crop_fields_from_openfarm_data.rb b/db/migrate/20240101010102_populate_crop_fields_from_openfarm_data.rb new file mode 100644 index 000000000..32c35aead --- /dev/null +++ b/db/migrate/20240101010102_populate_crop_fields_from_openfarm_data.rb @@ -0,0 +1,21 @@ +class PopulateCropFieldsFromOpenfarmData < ActiveRecord::Migration[5.2] + def up + Crop.find_each do |crop| + if crop.openfarm_data.present? + attributes = crop.openfarm_data.fetch('attributes', {}) + crop.update_columns( + row_spacing: attributes['row_spacing'], + spread: attributes['spread'], + height: attributes['height'], + sowing_method: attributes['sowing_method'], + sun_requirements: attributes['sun_requirements'], + growing_degree_days: attributes['growing_degree_days'] + ) + end + end + end + + def down + # This migration is not reversible. + end +end diff --git a/db/schema.rb b/db/schema.rb index b09d3956b..982f1e06a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -252,6 +252,12 @@ ActiveRecord::Schema[7.2].define(version: 2025_09_01_130830) do t.jsonb "openfarm_data" t.integer "harvests_count", default: 0 t.integer "photo_associations_count", default: 0 + t.integer "row_spacing" + t.integer "spread" + t.integer "height" + t.string "sowing_method" + t.string "sun_requirements" + t.integer "growing_degree_days" t.index ["creator_id"], name: "index_crops_on_creator_id" t.index ["name"], name: "index_crops_on_name" t.index ["parent_id"], name: "index_crops_on_parent_id" diff --git a/spec/controllers/crops_controller_spec.rb b/spec/controllers/crops_controller_spec.rb index 2dec49ae7..3b3b4ab10 100644 --- a/spec/controllers/crops_controller_spec.rb +++ b/spec/controllers/crops_controller_spec.rb @@ -100,6 +100,36 @@ describe CropsController do it { expect { subject }.to change(Crop, :count).by(1) } it { expect { subject }.to change(AlternateName, :count).by(2) } it { expect { subject }.to change(ScientificName, :count).by(1) } + + context 'with openfarm data' do + let(:crop_params) do + { + crop: { + name: 'aubergine', + en_wikipedia_url: "https://en.wikipedia.org/wiki/Eggplant", + row_spacing: 10, + spread: 20, + height: 30, + sowing_method: 'direct', + sun_requirements: 'full sun', + growing_degree_days: 100 + }, + alt_name: { '1': "egg plant", '2': "purple apple" }, + sci_name: { '1': "fancy sci name", '2': "" } + } + end + + it 'saves openfarm data' do + subject + crop = Crop.last + expect(crop.row_spacing).to eq(10) + expect(crop.spread).to eq(20) + expect(crop.height).to eq(30) + expect(crop.sowing_method).to eq('direct') + expect(crop.sun_requirements).to eq('full sun') + expect(crop.growing_degree_days).to eq(100) + end + end end end diff --git a/spec/features/crops/creating_a_crop_spec.rb b/spec/features/crops/creating_a_crop_spec.rb index afaaad7e2..f87a44fbb 100644 --- a/spec/features/crops/creating_a_crop_spec.rb +++ b/spec/features/crops/creating_a_crop_spec.rb @@ -19,6 +19,14 @@ describe "Crop", :js do click_button class: "add-altname-row" fill_in "alt_name[3]", with: "Jazmin" fill_in "alt_name[4]", with: "Matsurika" + + fill_in "crop_row_spacing", with: "12" + fill_in "crop_spread", with: "30" + fill_in "crop_height", with: "10" + fill_in "crop_sowing_method", with: "directly into final position" + + fill_in "crop_sun_requirements", with: "full sun" + fill_in "crop_growing_degree_days", with: 100 end end end From a21a9e7a0978fb530b609ed0c70caa04d1a3b7c8 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 8 Sep 2025 17:38:03 +0930 Subject: [PATCH 038/387] Update garden_resource to filter by owner --- app/resources/api/v1/garden_resource.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/resources/api/v1/garden_resource.rb b/app/resources/api/v1/garden_resource.rb index 4b4a32bbe..47b803fe6 100644 --- a/app/resources/api/v1/garden_resource.rb +++ b/app/resources/api/v1/garden_resource.rb @@ -10,6 +10,10 @@ module Api has_many :photos attribute :name + + filter :slug + filter :owner + filter :owner_id end end end From 5bfeb0ce03dbc67b74925bd1ee18d3fe4fd22e15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 07:01:07 +0000 Subject: [PATCH 039/387] Bump puma from 7.0.0 to 7.0.2 Bumps [puma](https://github.com/puma/puma) from 7.0.0 to 7.0.2. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v7.0.0...v7.0.2) --- updated-dependencies: - dependency-name: puma dependency-version: 7.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6247f9810..52f97356c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -475,7 +475,7 @@ GEM date stringio public_suffix (6.0.1) - puma (7.0.0) + puma (7.0.2) nio4r (~> 2.0) query_diet (0.7.2) racc (1.8.1) From cf840582d5221506a10660f3ad68d7e1554bb350 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 08:56:00 +0000 Subject: [PATCH 040/387] Bump factory_bot_rails from 6.5.0 to 6.5.1 Bumps [factory_bot_rails](https://github.com/thoughtbot/factory_bot_rails) from 6.5.0 to 6.5.1. - [Release notes](https://github.com/thoughtbot/factory_bot_rails/releases) - [Changelog](https://github.com/thoughtbot/factory_bot_rails/blob/main/NEWS.md) - [Commits](https://github.com/thoughtbot/factory_bot_rails/compare/v6.5.0...v6.5.1) --- updated-dependencies: - dependency-name: factory_bot_rails dependency-version: 6.5.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 52f97356c..41f0f1878 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -142,7 +142,7 @@ GEM erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.2.2) + bigdecimal (3.2.3) bluecloth (2.2.0) bonsai-elasticsearch-rails (7.0.1) elasticsearch-model (< 8) @@ -198,7 +198,7 @@ GEM comfy_bootstrap_form (4.0.9) rails (>= 5.0.0) concurrent-ruby (1.3.5) - connection_pool (2.5.3) + connection_pool (2.5.4) crass (1.0.6) crowdin-api (1.12.0) open-uri (>= 0.1.0, < 0.2.0) @@ -257,9 +257,9 @@ GEM excon (1.2.5) logger execjs (2.10.0) - factory_bot (6.5.4) + factory_bot (6.5.5) activesupport (>= 6.1.0) - factory_bot_rails (6.5.0) + factory_bot_rails (6.5.1) factory_bot (~> 6.5) railties (>= 6.1.0) faker (3.5.2) From e5c71f1dc4682e09bfa338875811947344ec9ddc Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 21:34:49 +0930 Subject: [PATCH 041/387] Merge pull request #4230 from Growstuff/feature/add-more-filters feat: Add more filters to API resources --- .../api/v1/activities_controller.rb | 10 ++++ app/resources/api/v1/activity_resource.rb | 27 +++++++++ app/resources/api/v1/garden_resource.rb | 7 ++- app/resources/api/v1/harvest_resource.rb | 9 +++ app/resources/api/v1/planting_resource.rb | 4 ++ app/resources/api/v1/seed_resource.rb | 9 +++ config/routes.rb | 1 + .../api/v1/activities_request_spec.rb | 56 +++++++++++++++++++ spec/requests/api/v1/gardens_request_spec.rb | 27 +++++++++ spec/requests/api/v1/harvests_request_spec.rb | 33 +++++++++++ .../requests/api/v1/plantings_request_spec.rb | 32 +++++++++++ spec/requests/api/v1/seeds_request_spec.rb | 52 +++++++++++++++++ 12 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 app/controllers/api/v1/activities_controller.rb create mode 100644 app/resources/api/v1/activity_resource.rb create mode 100644 spec/requests/api/v1/activities_request_spec.rb diff --git a/app/controllers/api/v1/activities_controller.rb b/app/controllers/api/v1/activities_controller.rb new file mode 100644 index 000000000..a79f9c778 --- /dev/null +++ b/app/controllers/api/v1/activities_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Api + module V1 + # This controller is intentionally empty. + # The `jsonapi-resources` gem provides the necessary actions. + class ActivitiesController < BaseController + end + end +end diff --git a/app/resources/api/v1/activity_resource.rb b/app/resources/api/v1/activity_resource.rb new file mode 100644 index 000000000..72256146b --- /dev/null +++ b/app/resources/api/v1/activity_resource.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Api + module V1 + class ActivityResource < BaseResource + immutable + + has_one :owner, class_name: 'Member' + has_one :garden + has_one :planting + + attribute :name + attribute :description + attribute :category + attribute :finished + attribute :due_date + + filter :owner + filter :owner_id + filter :garden + filter :garden_id + filter :planting + filter :planting_id + filter :category + end + end +end diff --git a/app/resources/api/v1/garden_resource.rb b/app/resources/api/v1/garden_resource.rb index 47b803fe6..47dcd7858 100644 --- a/app/resources/api/v1/garden_resource.rb +++ b/app/resources/api/v1/garden_resource.rb @@ -10,10 +10,13 @@ module Api has_many :photos attribute :name - - filter :slug + filter :owner filter :owner_id + filter :active + filter :garden_type + filter :location + filter :slug end end end diff --git a/app/resources/api/v1/harvest_resource.rb b/app/resources/api/v1/harvest_resource.rb index 502bee5bf..2013390a6 100644 --- a/app/resources/api/v1/harvest_resource.rb +++ b/app/resources/api/v1/harvest_resource.rb @@ -16,6 +16,15 @@ module Api attribute :weight_quantity attribute :weight_unit attribute :si_weight + + filter :owner + filter :owner_id + filter :crop + filter :crop_id + filter :planting + filter :planting_id + filter :plant_part + filter :harvested_at end end end diff --git a/app/resources/api/v1/planting_resource.rb b/app/resources/api/v1/planting_resource.rb index 4d24036fb..8a5bd4659 100644 --- a/app/resources/api/v1/planting_resource.rb +++ b/app/resources/api/v1/planting_resource.rb @@ -36,6 +36,10 @@ module Api filter :owner filter :owner_id filter :finished + filter :active, apply: ->(records, _value, _options) { records.active } + filter :failed, apply: ->(records, _value, _options) { records.failed } + filter :sunniness + filter :perennial, apply: ->(records, _value, _options) { records.perennial } attribute :percentage_grown delegate :percentage_grown, to: :@model diff --git a/app/resources/api/v1/seed_resource.rb b/app/resources/api/v1/seed_resource.rb index 82dd53ee2..dc1016cd9 100644 --- a/app/resources/api/v1/seed_resource.rb +++ b/app/resources/api/v1/seed_resource.rb @@ -17,6 +17,15 @@ module Api attribute :organic attribute :gmo attribute :heirloom + + filter :owner + filter :owner_id + filter :crop + filter :crop_id + filter :tradable_to + filter :organic + filter :gmo + filter :heirloom end end end diff --git a/config/routes.rb b/config/routes.rb index 084cf98e0..700acc657 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -141,6 +141,7 @@ Rails.application.routes.draw do namespace :api do namespace :v1 do + jsonapi_resources :activities jsonapi_resources :crops jsonapi_resources :gardens jsonapi_resources :harvests diff --git a/spec/requests/api/v1/activities_request_spec.rb b/spec/requests/api/v1/activities_request_spec.rb new file mode 100644 index 000000000..553c39856 --- /dev/null +++ b/spec/requests/api/v1/activities_request_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Activities', type: :request do + subject { JSON.parse response.body } + + let(:headers) { { 'Accept' => 'application/vnd.api+json' } } + let!(:activity) { FactoryBot.create(:activity, garden: create(:garden), planting: create(:planting)) } + let!(:activity2) { FactoryBot.create(:activity) } + + it '#index' do + get('/api/v1/activities', params: {}, headers:) + expect(subject['data'].size).to eq(2) + end + + it '#show' do + get("/api/v1/activities/#{activity.id}", params: {}, headers:) + expect(subject['data']['id']).to eq(activity.id.to_s) + end + + context 'filtering' do + it 'filters by owner' do + get("/api/v1/activities?filter[owner-id]=#{activity.owner.id}", params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) + end + + it 'filters by garden' do + get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) + end + + it 'filters by planting' do + get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) + end + + it 'filters by category' do + get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(2) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) + expect(subject['data'][1]['id']).to eq(activity2.id.to_s) + end + end +end diff --git a/spec/requests/api/v1/gardens_request_spec.rb b/spec/requests/api/v1/gardens_request_spec.rb index 601d835d9..344ea74d1 100644 --- a/spec/requests/api/v1/gardens_request_spec.rb +++ b/spec/requests/api/v1/gardens_request_spec.rb @@ -50,6 +50,33 @@ RSpec.describe 'Gardens', type: :request do expect(subject['data']).to include(garden_encoded_as_json_api) end + context 'filtering' do + let!(:garden2) { FactoryBot.create(:garden, active: false, garden_type: FactoryBot.create(:garden_type)) } + pending 'filters by active' do + get('/api/v1/gardens?filter[active]=true', params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(garden.id.to_s) + end + + it 'filters by garden_type' do + get("/api/v1/gardens?filter[garden_type]=#{garden2.garden_type.id}", params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(garden2.id.to_s) + end + + it 'filters by owner' do + get("/api/v1/gardens?filter[owner_id]=#{garden2.owner.id}", params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(2) + expect(subject['data'][1]['id']).to eq(garden2.id.to_s) + end + end + it '#create' do expect do post '/api/v1/gardens', params: { 'garden' => { 'name' => 'can i make this' } }, headers: diff --git a/spec/requests/api/v1/harvests_request_spec.rb b/spec/requests/api/v1/harvests_request_spec.rb index 7f0290544..f49b7b88f 100644 --- a/spec/requests/api/v1/harvests_request_spec.rb +++ b/spec/requests/api/v1/harvests_request_spec.rb @@ -76,6 +76,39 @@ RSpec.describe 'Harvests', type: :request do it { expect(subject['data']).to eq(harvest_encoded_as_json_api) } end + context 'filtering' do + let!(:harvest2) { FactoryBot.create(:harvest, planting: create(:planting)) } + it 'filters by crop' do + get("/api/v1/harvests?filter[crop_id]=#{harvest2.crop.id}", params: {}, headers:) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) + end + + it 'filters by planting' do + get("/api/v1/harvests?filter[planting_id]=#{harvest2.planting.id}", params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) + end + + it 'filters by plant_part' do + get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) + end + + it 'filters by owner' do + get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) + end + end + it '#create' do expect do put '/api/v1/harvests', headers:, params: { diff --git a/spec/requests/api/v1/plantings_request_spec.rb b/spec/requests/api/v1/plantings_request_spec.rb index b8b7bec12..5a406afe9 100644 --- a/spec/requests/api/v1/plantings_request_spec.rb +++ b/spec/requests/api/v1/plantings_request_spec.rb @@ -140,4 +140,36 @@ RSpec.describe 'Plantings', type: :request do end end end + + context 'filtering' do + let!(:planting2) { FactoryBot.create(:planting, failed: true, sunniness: 'shade') } + let!(:perennial_planting) { FactoryBot.create(:planting, crop: FactoryBot.create(:crop, perennial: true)) } + it 'filters by failed' do + get('/api/v1/plantings?filter[failed]=true', params: {}, headers:) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(planting2.id.to_s) + end + + it 'filters by sunniness' do + get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers:) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(planting2.id.to_s) + end + + it 'filters by perennial' do + get('/api/v1/plantings?filter[perennial]=true', params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(perennial_planting.id.to_s) + end + + it 'filters by active' do + get('/api/v1/plantings?filter[active]=true', params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(2) + expect(subject['data'][0]['id']).to eq(planting.id.to_s) + end + end end diff --git a/spec/requests/api/v1/seeds_request_spec.rb b/spec/requests/api/v1/seeds_request_spec.rb index d24b5bee1..00ee04684 100644 --- a/spec/requests/api/v1/seeds_request_spec.rb +++ b/spec/requests/api/v1/seeds_request_spec.rb @@ -78,4 +78,56 @@ RSpec.describe 'Seeds', type: :request do delete "/api/v1/seeds/#{seed.id}", params: {}, headers: end.to raise_error ActionController::RoutingError end + + context 'filtering' do + let!(:seed2) { FactoryBot.create(:seed, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom') } + it 'filters by crop' do + get("/api/v1/seeds?filter[crop]=#{seed2.crop.id}", params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(seed2.id.to_s) + end + + + it 'filters by tradable_to' do + get('/api/v1/seeds?filter[tradable_to]=nationally', params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(seed2.id.to_s) + end + + it 'filters by organic' do + get('/api/v1/seeds?filter[organic]=certified organic', params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(seed2.id.to_s) + end + + it 'filters by gmo' do + get('/api/v1/seeds?filter[gmo]=certified GMO-free', params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(seed2.id.to_s) + end + + it 'filters by heirloom' do + get('/api/v1/seeds?filter[heirloom]=heirloom', params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(seed2.id.to_s) + end + + it 'filters by owner' do + get("/api/v1/seeds?filter[owner_id]=#{seed2.owner.id}", params: {}, headers:) + + expect(response.status).to eq 200 + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(seed2.id.to_s) + end + end end From fb78bcb0b0a01ca079f0fdafb4631b1bf6667da8 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 9 Sep 2025 12:36:22 +0000 Subject: [PATCH 042/387] Add aliases --- app/models/activity.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/activity.rb b/app/models/activity.rb index cbdfbbf89..c879d4f22 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -30,4 +30,12 @@ class Activity < ApplicationRecord def to_s name end + + def garden_name + garden&.name + end + + def planting_name + planting&.name + end end From bb4e2dd788bd765e15a79e5a3a02493181a29965 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 9 Sep 2025 22:07:24 +0930 Subject: [PATCH 043/387] Add aliases (#4232) --- app/models/activity.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/activity.rb b/app/models/activity.rb index cbdfbbf89..c879d4f22 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -30,4 +30,12 @@ class Activity < ApplicationRecord def to_s name end + + def garden_name + garden&.name + end + + def planting_name + planting&.name + end end From 2f0b8e9d765eea34982025d64fec5712066e48e9 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 9 Sep 2025 12:40:06 +0000 Subject: [PATCH 044/387] Add aliases --- app/models/activity.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/activity.rb b/app/models/activity.rb index c879d4f22..b16c3deaf 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -35,7 +35,15 @@ class Activity < ApplicationRecord garden&.name end + def garden_slug + garden&.slug + end + def planting_name planting&.name end + + def planting_slug + planting&.slug + end end From b2e959aded70814af43283d10735aa72dd6c2f82 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 9 Sep 2025 12:44:40 +0000 Subject: [PATCH 045/387] Delegate --- app/models/activity.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/activity.rb b/app/models/activity.rb index b16c3deaf..30bde60e1 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -40,10 +40,10 @@ class Activity < ApplicationRecord end def planting_name - planting&.name + planting&.crop&.name end def planting_slug - planting&.slug + planting&.crop&.slug end end From e322871740ad66e86443ea0e6bcde91a93b834bf Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 9 Sep 2025 12:49:17 +0000 Subject: [PATCH 046/387] Fix UX --- app/views/plantings/show.html.haml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/plantings/show.html.haml b/app/views/plantings/show.html.haml index 21a6d0635..dc5a10f55 100644 --- a/app/views/plantings/show.html.haml +++ b/app/views/plantings/show.html.haml @@ -89,11 +89,11 @@ - else .col-md-12 %p Nothing is currently planned here. - - if @finished_activities&.size&.positive? - %h2 Finished activities for planting - .index-cards - - @finished_activities.each do |activity| - = render "activities/card", activity: activity + - if @finished_activities&.size&.positive? + %h2 Finished activities for planting + .index-cards + - @finished_activities.each do |activity| + = render "activities/card", activity: activity .col-md-4.col-xs-12 = render @planting.crop From cf8380029ac2654551295a333604065f0f3f49f4 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Wed, 10 Sep 2025 10:19:08 +0000 Subject: [PATCH 047/387] Rubocop --- lib/tasks/wikidata.rake | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/tasks/wikidata.rake b/lib/tasks/wikidata.rake index e1f117a67..8ce31b942 100644 --- a/lib/tasks/wikidata.rake +++ b/lib/tasks/wikidata.rake @@ -36,21 +36,21 @@ namespace :wikidata do aliases = wikidata_data['entities'][wikidata_id]['aliases'] aliases.each do |lang, values| values.each do |value| - unless AlternateName.exists?(name: value['value'], language: lang, crop: crop) - AlternateName.create!( - name: value['value'], - language: lang, - crop: crop, - creator: creator - ) - puts " Added alternate name: #{value['value']} (#{lang})" - end + next if AlternateName.exists?(name: value['value'], language: lang, crop: crop) + + AlternateName.create!( + name: value['value'], + language: lang, + crop: crop, + creator: creator + ) + puts " Added alternate name: #{value['value']} (#{lang})" end end else puts " Could not find Wikidata ID for #{crop.name}" end - rescue => e + rescue StandardError => e puts " Error processing crop #{crop.name}: #{e.message}" end end From 02db5b81306d5722f02136319f66c0ee51df5e3e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 19:50:06 +0930 Subject: [PATCH 048/387] Add API token generation, authentication, and CRUD for a number of the API resources (#4237) * feat: Add API token generation and authentication This commit introduces API token generation and authentication for write operations. - Adds a section to the user's profile edit page to generate and display an API token. - Reuses the `authentications` table to store the API token, avoiding the need for a database migration. - Implements token-based authentication for the API using the `Authorization: Token token=...` header. - Enables write operations for all API resources and ensures they are protected by the new authentication mechanism. - Adds feature and request specs to test the new functionality. * feat: Add API token generation and authentication This commit introduces API token generation and authentication for write operations. - Adds a section to the user's profile edit page to generate and display an API token. - Reuses the `authentications` table to store the API token, avoiding the need for a database migration. - Implements token-based authentication for the API using the `Authorization: Token token=...` header. - Enables write operations for all API resources and ensures they are protected by the new authentication mechanism. - Adds feature and request specs to test the new functionality. * Mark as editable * Refactor * WIP - Authentication * Implement more test coverage * Split 401 and 403 * Before Create hooks * Update harvest specs, defaulting to the first plant part - this may not be right * Update coverage * Update coverage * Rubocop * Rubocop * Rubocop * Fix coverage * For now, mark photos immutable again * Fix specs * Fix specs * Rubocop * Fix specs --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Daniel O'Connor --- .rubocop_todo.yml | 2 +- .../api/v1/activities_controller.rb | 2 - app/controllers/api/v1/base_controller.rb | 34 ++++ app/controllers/registrations_controller.rb | 6 + app/models/member.rb | 14 ++ app/resources/api/v1/activity_resource.rb | 4 +- app/resources/api/v1/crop_resource.rb | 3 +- app/resources/api/v1/garden_resource.rb | 4 +- app/resources/api/v1/harvest_resource.rb | 8 +- app/resources/api/v1/photo_resource.rb | 5 +- app/resources/api/v1/planting_resource.rb | 4 +- app/resources/api/v1/seed_resource.rb | 4 +- app/resources/base_resource.rb | 12 +- .../devise/registrations/_edit_apps.html.haml | 13 ++ config/initializers/jsonapi_resources.rb | 4 +- config/locales/devise.en.yml | 1 + config/routes.rb | 1 + .../features/members/token_management_spec.rb | 38 +++++ .../api/v1/activities_request_spec.rb | 28 ++-- spec/requests/api/v1/gardens_request_spec.rb | 122 +++++++++++++-- spec/requests/api/v1/harvests_request_spec.rb | 145 ++++++++++++++--- .../requests/api/v1/plantings_request_spec.rb | 146 +++++++++++++++--- spec/requests/api/v1/seeds_request_spec.rb | 137 +++++++++++++--- 23 files changed, 624 insertions(+), 113 deletions(-) create mode 100644 spec/features/members/token_management_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3195873c1..fbba7cede 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -314,7 +314,7 @@ RSpec/MultipleExpectations: # Offense count: 138 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: - Max: 14 + Max: 20 # Offense count: 133 # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. diff --git a/app/controllers/api/v1/activities_controller.rb b/app/controllers/api/v1/activities_controller.rb index a79f9c778..eaeb4c085 100644 --- a/app/controllers/api/v1/activities_controller.rb +++ b/app/controllers/api/v1/activities_controller.rb @@ -2,8 +2,6 @@ module Api module V1 - # This controller is intentionally empty. - # The `jsonapi-resources` gem provides the necessary actions. class ActivitiesController < BaseController end end diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 84eb42dcc..bf69de691 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -4,6 +4,40 @@ module Api module V1 class BaseController < JSONAPI::ResourceController abstract + protect_from_forgery with: :null_session + before_action :authenticate_member_from_token! + before_action :enforce_member_for_write_operations!, only: %i(create update destroy) + rescue_from CanCan::AccessDenied do + head :forbidden + end + + def context + { + current_user: current_user, + current_ability: current_ability, + controller: self, + action: params[:action] + } + end + + private + + attr_reader :current_user + + def enforce_member_for_write_operations! + head :unauthorized unless current_user + end + + def authenticate_member_from_token! + authenticate_with_http_token do |token, _options| + auth = Authentication.find_by(token: token, provider: 'api') + if auth.present? + @current_user = auth.member + + return true + end + end + end end end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 2bb8b1764..90593eaca 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -38,6 +38,12 @@ class RegistrationsController < Devise::RegistrationsController end end + def regenerate_api_token + current_member.regenerate_api_token + set_flash_message :notice, :api_token_regenerated + redirect_to edit_member_registration_path + '#apps' + end + def destroy if @member.valid_password?(params.require(:member)[:current_password]) @member.discard diff --git a/app/models/member.rb b/app/models/member.rb index 2427092f6..f9b1c6942 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -24,6 +24,20 @@ class Member < ApplicationRecord has_many :notifications, foreign_key: 'recipient_id', inverse_of: :recipient has_many :sent_notifications, foreign_key: 'sender_id', inverse_of: :sender, class_name: "Notification" has_many :authentications, dependent: :destroy + has_one :api_token, -> { where(provider: 'api') }, class_name: 'Authentication', dependent: :destroy + + def api_token? + api_token.present? + end + + def regenerate_api_token + api_token.destroy if api_token? + create_api_token( + provider: 'api', + uid: id, + token: SecureRandom.hex(16) + ) + end has_many :photos, inverse_of: :owner has_many :likes, dependent: :destroy diff --git a/app/resources/api/v1/activity_resource.rb b/app/resources/api/v1/activity_resource.rb index 72256146b..5803a7686 100644 --- a/app/resources/api/v1/activity_resource.rb +++ b/app/resources/api/v1/activity_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class ActivityResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_one :garden diff --git a/app/resources/api/v1/crop_resource.rb b/app/resources/api/v1/crop_resource.rb index 789884392..ed4cac67c 100644 --- a/app/resources/api/v1/crop_resource.rb +++ b/app/resources/api/v1/crop_resource.rb @@ -3,8 +3,7 @@ module Api module V1 class CropResource < BaseResource - immutable - + immutable # TODO: Re-evaluate this later filter :approval_status, default: 'approved' has_many :plantings diff --git a/app/resources/api/v1/garden_resource.rb b/app/resources/api/v1/garden_resource.rb index 47dcd7858..cc94847a3 100644 --- a/app/resources/api/v1/garden_resource.rb +++ b/app/resources/api/v1/garden_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class GardenResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_many :plantings diff --git a/app/resources/api/v1/harvest_resource.rb b/app/resources/api/v1/harvest_resource.rb index 2013390a6..8f086dc55 100644 --- a/app/resources/api/v1/harvest_resource.rb +++ b/app/resources/api/v1/harvest_resource.rb @@ -3,11 +3,17 @@ module Api module V1 class HarvestResource < BaseResource - immutable + before_save do + @model.owner = context[:current_user] + @model.crop_id = @model.planting.crop_id if @model.planting_id + @model.harvested_at = Time.zone.now if @model.harvested_at.blank? + @model.plant_part = PlantPart.first + end has_one :crop has_one :planting has_one :owner, class_name: 'Member' + # has_one :plant_part has_many :photos attribute :harvested_at diff --git a/app/resources/api/v1/photo_resource.rb b/app/resources/api/v1/photo_resource.rb index 6da294cd8..4f6c08223 100644 --- a/app/resources/api/v1/photo_resource.rb +++ b/app/resources/api/v1/photo_resource.rb @@ -3,7 +3,10 @@ module Api module V1 class PhotoResource < BaseResource - immutable + immutable # TODO: Re-evaluate this. + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_many :plantings diff --git a/app/resources/api/v1/planting_resource.rb b/app/resources/api/v1/planting_resource.rb index 8a5bd4659..3ab2c4fc8 100644 --- a/app/resources/api/v1/planting_resource.rb +++ b/app/resources/api/v1/planting_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class PlantingResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :garden has_one :crop diff --git a/app/resources/api/v1/seed_resource.rb b/app/resources/api/v1/seed_resource.rb index dc1016cd9..9c69e493a 100644 --- a/app/resources/api/v1/seed_resource.rb +++ b/app/resources/api/v1/seed_resource.rb @@ -3,7 +3,9 @@ module Api module V1 class SeedResource < BaseResource - immutable + before_create do + @model.owner = context[:current_user] + end has_one :owner, class_name: 'Member' has_one :crop diff --git a/app/resources/base_resource.rb b/app/resources/base_resource.rb index 46de0ce53..b85ba45e1 100644 --- a/app/resources/base_resource.rb +++ b/app/resources/base_resource.rb @@ -1,6 +1,16 @@ # frozen_string_literal: true class BaseResource < JSONAPI::Resource - immutable abstract + + [:create, :update, :remove].each do |action| + set_callback action, :before, :authorize + end + + # Check authorisation for write operations. + # NOTE: At a later time, we may require API tokens for READ operations. + def authorize + # context[:action] is simply context[:controller].params[:action] + context[:current_ability].authorize! context[:action].to_sym, @model + end end diff --git a/app/views/devise/registrations/_edit_apps.html.haml b/app/views/devise/registrations/_edit_apps.html.haml index 12e267bb0..24fb472e4 100644 --- a/app/views/devise/registrations/_edit_apps.html.haml +++ b/app/views/devise/registrations/_edit_apps.html.haml @@ -15,3 +15,16 @@ method: :delete, class: "remove btn btn-danger" - else = link_to 'Connect to Flickr', '/members/auth/flickr', class: 'btn' + %hr + .row + .col-md-12 + %p + = image_tag "icons/post.svg", size: "32x32", alt: 'API logo' + - if current_member.api_token? + Your API token is + %code= current_member.api_token.token + = link_to "Regenerate", regenerate_api_token_path, + data: { confirm: "Are you sure? Your old token will stop working immediately." }, + method: :post, class: "remove btn btn-danger" + - else + = link_to 'Generate API Token', regenerate_api_token_path, method: :post, class: 'btn btn-primary' diff --git a/config/initializers/jsonapi_resources.rb b/config/initializers/jsonapi_resources.rb index 8cf1a906b..7f0c57c29 100644 --- a/config/initializers/jsonapi_resources.rb +++ b/config/initializers/jsonapi_resources.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true - +class UnauthorisedError < JSONAPI::Error +end JSONAPI.configure do |config| # built in paginators are :none, :offset, :paged config.default_paginator = :offset config.default_page_size = 10 config.maximum_page_size = 100 + config.exception_class_whitelist = [CanCan::AccessDenied, UnauthorisedError] end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 3fa1481e0..ffd459cf5 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -54,6 +54,7 @@ en: You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address. destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.' + api_token_regenerated: 'Your API token has been regenerated.' unlocks: send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.' unlocked: 'Your account has been unlocked successfully. Please sign in to continue.' diff --git a/config/routes.rb b/config/routes.rb index 700acc657..3b68bc8f1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,6 +16,7 @@ Rails.application.routes.draw do } devise_scope :member do get '/members/unsubscribe/:message' => 'members#unsubscribe', as: 'unsubscribe_member' + post '/members/regenerate_api_token' => 'registrations#regenerate_api_token', as: 'regenerate_api_token' end match '/members/:id/finish_signup' => 'members#finish_signup', via: %i(get patch), as: :finish_signup diff --git a/spec/features/members/token_management_spec.rb b/spec/features/members/token_management_spec.rb new file mode 100644 index 000000000..6fcc77366 --- /dev/null +++ b/spec/features/members/token_management_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe "member token management", :js do + include_context 'signed in member' + + before do + visit edit_member_registration_path + click_on "Apps" + end + + it "can generate an API token" do + expect(page).to have_no_content("Your API token is") + click_on "Generate API Token" + expect(page).to have_content("Your API token is") + member.reload + expect(member.api_token).to be_present + end + + context "with an existing token" do + before do + member.regenerate_api_token + visit edit_member_registration_path + click_on "Apps" + end + + it "can regenerate an API token" do + old_token = member.api_token.token + expect(page).to have_content("Your API token is") + accept_confirm do + click_on "Regenerate" + end + expect(page).to have_content("Your API token is") + expect(member.reload.api_token.token).not_to eq(old_token) + end + end +end diff --git a/spec/requests/api/v1/activities_request_spec.rb b/spec/requests/api/v1/activities_request_spec.rb index 553c39856..0eb5e98a8 100644 --- a/spec/requests/api/v1/activities_request_spec.rb +++ b/spec/requests/api/v1/activities_request_spec.rb @@ -23,34 +23,34 @@ RSpec.describe 'Activities', type: :request do it 'filters by owner' do get("/api/v1/activities?filter[owner-id]=#{activity.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(activity.id.to_s) end it 'filters by garden' do - get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers:) + get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(activity.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) end it 'filters by planting' do - get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers:) + get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(activity.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) end it 'filters by category' do - get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers:) + get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(2) - expect(subject['data'][0]['id']).to eq(activity.id.to_s) - expect(subject['data'][1]['id']).to eq(activity2.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(2) + expect(subject['data'][0]['id']).to eq(activity.id.to_s) + expect(subject['data'][1]['id']).to eq(activity2.id.to_s) end end end diff --git a/spec/requests/api/v1/gardens_request_spec.rb b/spec/requests/api/v1/gardens_request_spec.rb index 344ea74d1..de3906db8 100644 --- a/spec/requests/api/v1/gardens_request_spec.rb +++ b/spec/requests/api/v1/gardens_request_spec.rb @@ -52,18 +52,19 @@ RSpec.describe 'Gardens', type: :request do context 'filtering' do let!(:garden2) { FactoryBot.create(:garden, active: false, garden_type: FactoryBot.create(:garden_type)) } + pending 'filters by active' do get('/api/v1/gardens?filter[active]=true', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(garden.id.to_s) end it 'filters by garden_type' do get("/api/v1/gardens?filter[garden_type]=#{garden2.garden_type.id}", params: {}, headers:) - - expect(response.status).to eq 200 + + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(garden2.id.to_s) end @@ -71,27 +72,116 @@ RSpec.describe 'Gardens', type: :request do it 'filters by owner' do get("/api/v1/gardens?filter[owner_id]=#{garden2.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(2) expect(subject['data'][1]['id']).to eq(garden2.id.to_s) end end - it '#create' do - expect do - post '/api/v1/gardens', params: { 'garden' => { 'name' => 'can i make this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:garden_params) do + { + data: { + type: 'gardens', + attributes: { + name: 'My API Garden' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/gardens', params: garden_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/gardens', params: garden_params, headers: auth_headers + expect(response).to have_http_status(:created) + expect(member.gardens.count).to eq(2) # 1 from after_create callback, 1 from api + end end - it '#update' do - expect do - post "/api/v1/gardens/#{garden.id}", params: { 'garden' => { 'name' => 'can i modify this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:garden) { create(:garden, owner: member) } + let(:other_member_garden) { create(:garden) } + let(:update_params) do + { + data: { + type: 'gardens', + id: garden.id.to_s, + attributes: { + name: 'An updated garden' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own garden' do + patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: auth_headers + expect(response).to have_http_status(:ok) + expect(garden.reload.name).to eq('An updated garden') + end + + it 'returns 403 Forbidden for another member\'s garden' do + update_params_for_other = { + data: { + type: 'gardens', + id: other_member_garden.id.to_s, + attributes: { + name: 'An updated garden' + } + } + }.to_json + patch "/api/v1/gardens/#{other_member_garden.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/gardens/#{garden.id}", params: {}, headers: - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let!(:garden) { create(:garden, owner: member) } + let(:other_member_garden) { create(:garden) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/gardens/#{garden.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own garden' do + delete "/api/v1/gardens/#{garden.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Garden.find_by(id: garden.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s garden' do + delete "/api/v1/gardens/#{other_member_garden.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end end diff --git a/spec/requests/api/v1/harvests_request_spec.rb b/spec/requests/api/v1/harvests_request_spec.rb index f49b7b88f..38d6777c1 100644 --- a/spec/requests/api/v1/harvests_request_spec.rb +++ b/spec/requests/api/v1/harvests_request_spec.rb @@ -78,6 +78,7 @@ RSpec.describe 'Harvests', type: :request do context 'filtering' do let!(:harvest2) { FactoryBot.create(:harvest, planting: create(:planting)) } + it 'filters by crop' do get("/api/v1/harvests?filter[crop_id]=#{harvest2.crop.id}", params: {}, headers:) expect(subject['data'].size).to eq(1) @@ -87,47 +88,141 @@ RSpec.describe 'Harvests', type: :request do it 'filters by planting' do get("/api/v1/harvests?filter[planting_id]=#{harvest2.planting.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) end it 'filters by plant_part' do - get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers:) + get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) end it 'filters by owner' do - get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers:) + get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(harvest2.id.to_s) end end - it '#create' do - expect do - put '/api/v1/harvests', headers:, params: { - 'harvest' => { 'description' => 'can i make this' } - } - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:planting) { create(:planting, owner: member) } + let(:plant_part) { create(:plant_part) } + let(:harvest_params) do + { + data: { + type: 'harvests', + attributes: { + description: 'My API harvests' + }, + relationships: { + planting: { data: { type: 'plantings', id: planting.id } } + # plant_part: { data: { type: 'plant_parts', id: plant_part.id } } + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/harvests', params: harvest_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/harvests', params: harvest_params, headers: auth_headers + + expect(response).to have_http_status(:created) + expect(member.harvests.count).to eq(1) + end end - it '#update' do - expect do - post "/api/v1/harvests/#{harvest.id}", headers:, params: { - 'harvest' => { 'description' => 'can i modify this' } - } - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:harvest) { create(:harvest, owner: member) } + let(:other_member_harvest) { create(:harvest) } + let(:update_params) do + { + data: { + type: 'harvests', + id: harvest.id.to_s, + attributes: { + description: 'An updated harvest' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own harvest' do + patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: auth_headers + + expect(response).to have_http_status(:ok) + expect(harvest.reload.description).to eq('An updated harvest') + end + + it 'returns 403 Forbidden for another member\'s harvest' do + update_params_for_other = { + data: { + type: 'harvests', + id: other_member_harvest.id.to_s, + attributes: { + description: 'An updated harvest' + } + } + }.to_json + patch "/api/v1/harvests/#{other_member_harvest.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/harvests/#{harvest.id}", headers:, params: {} - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let!(:harvest) { create(:harvest, owner: member) } + let(:other_member_harvest) { create(:harvest) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/harvests/#{harvest.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own harvest' do + delete "/api/v1/harvests/#{harvest.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Garden.find_by(id: harvest.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s harvest' do + delete "/api/v1/harvests/#{other_member_harvest.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end end diff --git a/spec/requests/api/v1/plantings_request_spec.rb b/spec/requests/api/v1/plantings_request_spec.rb index 5a406afe9..7d334d539 100644 --- a/spec/requests/api/v1/plantings_request_spec.rb +++ b/spec/requests/api/v1/plantings_request_spec.rb @@ -95,24 +95,119 @@ RSpec.describe 'Plantings', type: :request do expect(subject['data']).to eq(planting_encoded_as_json_api) end - it '#create' do - expect do - post '/api/v1/plantings', params: { 'planting' => { 'description' => 'can i make this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:garden) { create(:garden, owner: member) } + let(:planting_params) do + { + data: { + type: 'plantings', + attributes: { + description: 'My API plantings' + }, + relationships: { + crop: { data: { type: 'crops', id: crop.id } }, + garden: { data: { type: 'gardens', id: garden.id } } + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/plantings', params: planting_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/plantings', params: planting_params, headers: auth_headers + + expect(response).to have_http_status(:created) + expect(member.plantings.count).to eq(1) + end end - it '#update' do - expect do - post "/api/v1/plantings/#{planting.id}", headers:, params: { - 'planting' => { 'description' => 'can i modify this' } - } - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:planting) { create(:planting, owner: member) } + let(:other_member_planting) { create(:planting) } + let(:update_params) do + { + data: { + type: 'plantings', + id: planting.id.to_s, + attributes: { + description: 'An updated planting' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own planting' do + patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: auth_headers + + expect(response).to have_http_status(:ok) + expect(planting.reload.description).to eq('An updated planting') + end + + it 'returns 403 Forbidden for another member\'s planting' do + update_params_for_other = { + data: { + type: 'plantings', + id: other_member_planting.id.to_s, + attributes: { + description: 'An updated planting' + } + } + }.to_json + patch "/api/v1/plantings/#{other_member_planting.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/plantings/#{planting.id}", params: {}, headers: - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let!(:planting) { create(:planting, owner: member) } + let(:other_member_planting) { create(:planting) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/plantings/#{planting.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own planting' do + delete "/api/v1/plantings/#{planting.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Garden.find_by(id: planting.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s planting' do + delete "/api/v1/plantings/#{other_member_planting.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end describe "by member/owner" do @@ -144,6 +239,7 @@ RSpec.describe 'Plantings', type: :request do context 'filtering' do let!(:planting2) { FactoryBot.create(:planting, failed: true, sunniness: 'shade') } let!(:perennial_planting) { FactoryBot.create(:planting, crop: FactoryBot.create(:crop, perennial: true)) } + it 'filters by failed' do get('/api/v1/plantings?filter[failed]=true', params: {}, headers:) expect(subject['data'].size).to eq(1) @@ -151,25 +247,25 @@ RSpec.describe 'Plantings', type: :request do end it 'filters by sunniness' do - get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers:) - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(planting2.id.to_s) + get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers:) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(planting2.id.to_s) end it 'filters by perennial' do - get('/api/v1/plantings?filter[perennial]=true', params: {}, headers:) + get('/api/v1/plantings?filter[perennial]=true', params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(1) - expect(subject['data'][0]['id']).to eq(perennial_planting.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(1) + expect(subject['data'][0]['id']).to eq(perennial_planting.id.to_s) end it 'filters by active' do - get('/api/v1/plantings?filter[active]=true', params: {}, headers:) + get('/api/v1/plantings?filter[active]=true', params: {}, headers:) - expect(response.status).to eq 200 - expect(subject['data'].size).to eq(2) - expect(subject['data'][0]['id']).to eq(planting.id.to_s) + expect(response).to have_http_status(:ok) + expect(subject['data'].size).to eq(2) + expect(subject['data'][0]['id']).to eq(planting.id.to_s) end end end diff --git a/spec/requests/api/v1/seeds_request_spec.rb b/spec/requests/api/v1/seeds_request_spec.rb index 00ee04684..ea30c7924 100644 --- a/spec/requests/api/v1/seeds_request_spec.rb +++ b/spec/requests/api/v1/seeds_request_spec.rb @@ -61,39 +61,136 @@ RSpec.describe 'Seeds', type: :request do it { expect(subject['data']).to eq(seed_encoded_as_json_api) } end - it '#create' do - expect do - post '/api/v1/seeds', params: { 'seed' => { 'name' => 'can i make this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#create' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:seed_params) do + { + data: { + type: 'seeds', + attributes: { + description: 'My API seeds' + }, + relationships: { + crop: { data: { type: 'crops', id: crop.id } } + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + post '/api/v1/seeds', params: seed_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 201 Created with a valid token' do + post '/api/v1/seeds', params: seed_params, headers: auth_headers + expect(response).to have_http_status(:created) + expect(member.seeds.count).to eq(1) + end end - it '#update' do - expect do - post "/api/v1/seeds/#{seed.id}", params: { 'seed' => { 'name' => 'can i modify this' } }, headers: - end.to raise_error ActionController::RoutingError + describe '#update' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let(:seed) { create(:seed, owner: member, crop: crop) } + let(:other_member_seed) { create(:seed) } + let(:update_params) do + { + data: { + type: 'seeds', + id: seed.id.to_s, + attributes: { + description: 'An updated seed' + } + } + }.to_json + end + + it 'returns 401 Unauthorized without a token' do + patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 200 OK with a valid token for own seed' do + patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: auth_headers + expect(response).to have_http_status(:ok) + expect(seed.reload.description).to eq('An updated seed') + end + + it 'returns 403 Forbidden for another member\'s seed' do + update_params_for_other = { + data: { + type: 'seeds', + id: other_member_seed.id.to_s, + attributes: { + description: 'An updated seed' + } + } + }.to_json + patch "/api/v1/seeds/#{other_member_seed.id}", params: update_params_for_other, headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end - it '#delete' do - expect do - delete "/api/v1/seeds/#{seed.id}", params: {}, headers: - end.to raise_error ActionController::RoutingError + describe '#delete' do + let!(:member) { create(:member) } + let(:token) do + member.regenerate_api_token + member.api_token.token + end + let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } } + let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") } + let(:crop) { create(:crop) } + let!(:seed) { create(:seed, owner: member, crop: crop) } + let(:other_member_seed) { create(:seed) } + + it 'returns 401 Unauthorized without a token' do + delete "/api/v1/seeds/#{seed.id}", headers: headers + expect(response).to have_http_status(:unauthorized) + end + + it 'returns 204 No Content with a valid token for own seed' do + delete "/api/v1/seeds/#{seed.id}", headers: auth_headers + expect(response).to have_http_status(:no_content) + expect(Seed.find_by(id: seed.id)).to be_nil + end + + it 'returns 403 Forbidden for another member\'s seed' do + delete "/api/v1/seeds/#{other_member_seed.id}", headers: auth_headers + expect(response).to have_http_status(:forbidden) + end end context 'filtering' do - let!(:seed2) { FactoryBot.create(:seed, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom') } + let!(:seed2) do + FactoryBot.create(:seed, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom') + end + it 'filters by crop' do get("/api/v1/seeds?filter[crop]=#{seed2.crop.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end - it 'filters by tradable_to' do get('/api/v1/seeds?filter[tradable_to]=nationally', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -101,7 +198,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by organic' do get('/api/v1/seeds?filter[organic]=certified organic', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -109,7 +206,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by gmo' do get('/api/v1/seeds?filter[gmo]=certified GMO-free', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -117,7 +214,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by heirloom' do get('/api/v1/seeds?filter[heirloom]=heirloom', params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end @@ -125,7 +222,7 @@ RSpec.describe 'Seeds', type: :request do it 'filters by owner' do get("/api/v1/seeds?filter[owner_id]=#{seed2.owner.id}", params: {}, headers:) - expect(response.status).to eq 200 + expect(response).to have_http_status(:ok) expect(subject['data'].size).to eq(1) expect(subject['data'][0]['id']).to eq(seed2.id.to_s) end From 7988080054e3504cd0d938fb1a433e7bb0461464 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Wed, 10 Sep 2025 19:52:44 +0930 Subject: [PATCH 049/387] Update .rubocop.yml --- .rubocop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index c2370187b..a6b7784fa 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,5 @@ inherit_from: .rubocop_todo.yml -require: +plugins: - rubocop-factory_bot - rubocop-capybara - rubocop-rails From e5bf9d98e6905b5699f427bb66d97cc32229469b Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Wed, 10 Sep 2025 19:56:12 +0930 Subject: [PATCH 050/387] Rubocop (#4241) --- app/controllers/activities_controller.rb | 2 +- app/controllers/registrations_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb index 08e8aa4bd..8ca7f956e 100644 --- a/app/controllers/activities_controller.rb +++ b/app/controllers/activities_controller.rb @@ -29,7 +29,7 @@ class ActivitiesController < DataController def new @activity = Activity.new( - owner: current_member, + owner: current_member, due_date: Date.today ) if params[:garden_id] diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 90593eaca..88c569c83 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -6,7 +6,7 @@ class RegistrationsController < Devise::RegistrationsController prepend_before_action :check_captcha, only: [:create] # Change this to be any actions you want to protect with recaptcha. def edit - @flickr_auth = current_member.auth('flickr') + @flickr_auth = current_member.auth('flickr') render "edit" end From a76ef6a11756c8eae2c2bd6f008719255de5462e Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Wed, 10 Sep 2025 13:56:29 +0000 Subject: [PATCH 051/387] Format --- swagger/v1/swagger.json | 4748 +++++++++++++++++++-------------------- 1 file changed, 2374 insertions(+), 2374 deletions(-) diff --git a/swagger/v1/swagger.json b/swagger/v1/swagger.json index 9ac938a56..64531dc66 100644 --- a/swagger/v1/swagger.json +++ b/swagger/v1/swagger.json @@ -1,114 +1,388 @@ { "swagger": "2.0", "info": { - "title": "API V1", - "version": "V1" -}, - "basePath" : "/api/v1", + "title": "API V1", + "version": "V1" + }, + "basePath": "/api/v1", "paths": { - "/crops": { - "get": { - "summary": "crops List", - "tags": [ - "crops" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,name,en_wikipedia_url,perennial,median_lifespan,median_days_to_first_harvest,median_days_to_last_harvest", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[approval_status]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false + "/crops": { + "get": { + "summary": "crops List", + "tags": [ + "crops" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,name,en_wikipedia_url,perennial,median_lifespan,median_days_to_first_harvest,median_days_to_last_harvest", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[approval_status]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, + "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "name": { + "type": "string", + "x-nullable": false, + "description": "Name" + }, + "en_wikipedia_url": { + "type": "string", + "x-nullable": true, + "description": "Wikipedia URL (English)" + }, + "perennial": { + "type": "boolean", + "x-nullable": true, + "description": "Is the item perennial? (A plant that lives more than two years)" + }, + "median_lifespan": { + "type": "integer", + "x-nullable": true, + "description": "Median lifespan" + }, + "median_days_to_first_harvest": { + "type": "integer", + "x-nullable": true, + "description": "Median days to first harvest" + }, + "median_days_to_last_harvest": { + "type": "integer", + "x-nullable": true, + "description": "Median days to last harvest" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "seeds": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "parent": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" + } + } + }, + "description": "Data" + }, + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } + } } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + } + }, + "/crops/{id}": { + "get": { + "summary": "crops Detail", + "tags": [ + "crops" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -261,381 +535,309 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/crops/{id}": { - "get": { - "summary": "crops Detail", - "tags": [ - "crops" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/gardens": { + "get": { + "summary": "gardens List", + "tags": [ + "gardens" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,name", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "name": { + "type": "string", + "x-nullable": false, + "description": "Name" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "name": { - "type": "string", - "x-nullable": false, - "description": "Name" - }, - "en_wikipedia_url": { - "type": "string", - "x-nullable": true, - "description": "Wikipedia URL (English)" - }, - "perennial": { - "type": "boolean", - "x-nullable": true, - "description": "Is the item perennial? (A plant that lives more than two years)" - }, - "median_lifespan": { - "type": "integer", - "x-nullable": true, - "description": "Median lifespan" - }, - "median_days_to_first_harvest": { - "type": "integer", - "x-nullable": true, - "description": "Median days to first harvest" - }, - "median_days_to_last_harvest": { - "type": "integer", - "x-nullable": true, - "description": "Median days to last harvest" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "plantings": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "seeds": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "parent": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/gardens": { - "get": { - "summary": "gardens List", - "tags": [ - "gardens" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,name", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/gardens/{id}": { + "get": { + "summary": "gardens Detail", + "tags": [ + "gardens" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -723,337 +925,396 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/gardens/{id}": { - "get": { - "summary": "gardens Detail", - "tags": [ - "gardens" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/members": { + "get": { + "summary": "members List", + "tags": [ + "members" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,login_name,slug", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[login_name]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[slug]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" - } - }, - "description": "Detail link" - }, - "attributes": { - "type": "object", - "properties": { - "name": { - "type": "string", - "x-nullable": false, - "description": "Name" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "owner": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" }, - "plantings": { + "attributes": { "type": "object", "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" + "login_name": { + "type": "string", + "x-nullable": true, + "description": "Login name" + }, + "slug": { + "type": "string", + "x-nullable": true, + "description": "Slug" } }, - "description": "Related model" + "description": "Attributes" }, - "photos": { + "relationships": { "type": "object", "properties": { - "links": { + "gardens": { "type": "object", "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, "description": "Related link" } }, - "description": "Related link" + "description": "Related model" + }, + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "seeds": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" } }, - "description": "Related model" + "description": "Associate data" } - }, - "description": "Associate data" - } + } + }, + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/members": { - "get": { - "summary": "members List", - "tags": [ - "members" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,login_name,slug", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[login_name]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[slug]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/members/{id}": { + "get": { + "summary": "members Detail", + "tags": [ + "members" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -1186,375 +1447,368 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/members/{id}": { - "get": { - "summary": "members Detail", - "tags": [ - "members" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/harvests": { + "get": { + "summary": "harvests List", + "tags": [ + "harvests" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,harvested_at,description,unit,weight_quantity,weight_unit,si_weight", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "harvested_at": { + "type": "string", + "x-nullable": true, + "description": "Harvested date time" + }, + "description": { + "type": "string", + "x-nullable": true, + "description": "Description" + }, + "unit": { + "type": "string", + "x-nullable": true, + "description": "Unit" + }, + "weight_quantity": { + "type": "string", + "x-nullable": true, + "description": "Weight/Quanitity" + }, + "weight_unit": { + "type": "string", + "x-nullable": true, + "description": "Weight Unit" + }, + "si_weight": { + "type": "string", + "x-nullable": true, + "description": "SI Weight" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "crop": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "planting": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "login_name": { - "type": "string", - "x-nullable": true, - "description": "Login name" - }, - "slug": { - "type": "string", - "x-nullable": true, - "description": "Slug" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "gardens": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "plantings": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "seeds": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/harvests": { - "get": { - "summary": "harvests List", - "tags": [ - "harvests" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,harvested_at,description,unit,weight_quantity,weight_unit,si_weight", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/harvests/{id}": { + "get": { + "summary": "harvests Detail", + "tags": [ + "harvests" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -1571,7 +1825,7 @@ "harvested_at": { "type": "string", "x-nullable": true, - "description": "Harvested date time" + "description": "Harvested datetime" }, "description": { "type": "string", @@ -1586,7 +1840,7 @@ "weight_quantity": { "type": "string", "x-nullable": true, - "description": "Weight/Quanitity" + "description": "Weight/Quantity" }, "weight_unit": { "type": "string", @@ -1687,354 +1941,315 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/harvests/{id}": { - "get": { - "summary": "harvests Detail", - "tags": [ - "harvests" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/seeds": { + "get": { + "summary": "seeds List", + "tags": [ + "seeds" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,description,quantity,plant_before,tradable_to,days_until_maturity_min,days_until_maturity_max,organic,gmo,heirloom", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "x-nullable": true, + "description": "Description" + }, + "quantity": { + "type": "integer", + "x-nullable": true, + "description": "Quanitity" + }, + "plant_before": { + "type": "string", + "x-nullable": true, + "description": "Plant before" + }, + "tradable_to": { + "type": "string", + "x-nullable": true, + "description": "Tradeable to" + }, + "days_until_maturity_min": { + "type": "integer", + "x-nullable": true, + "description": "Days until maturity (min)" + }, + "days_until_maturity_max": { + "type": "integer", + "x-nullable": true, + "description": "Days until maturity (max)" + }, + "organic": { + "type": "string", + "x-nullable": true, + "description": "Organic" + }, + "gmo": { + "type": "string", + "x-nullable": true, + "description": "GMO" + }, + "heirloom": { + "type": "string", + "x-nullable": true, + "description": "Heirloom" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "crop": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "harvested_at": { - "type": "string", - "x-nullable": true, - "description": "Harvested datetime" - }, - "description": { - "type": "string", - "x-nullable": true, - "description": "Description" - }, - "unit": { - "type": "string", - "x-nullable": true, - "description": "Unit" - }, - "weight_quantity": { - "type": "string", - "x-nullable": true, - "description": "Weight/Quantity" - }, - "weight_unit": { - "type": "string", - "x-nullable": true, - "description": "Weight Unit" - }, - "si_weight": { - "type": "string", - "x-nullable": true, - "description": "SI Weight" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "crop": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "planting": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "owner": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/seeds": { - "get": { - "summary": "seeds List", - "tags": [ - "seeds" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,description,quantity,plant_before,tradable_to,days_until_maturity_min,days_until_maturity_max,organic,gmo,heirloom", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/seeds/{id}": { + "get": { + "summary": "seeds Detail", + "tags": [ + "seeds" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[seeds]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -2056,7 +2271,7 @@ "quantity": { "type": "integer", "x-nullable": true, - "description": "Quanitity" + "description": "Quanity" }, "plant_before": { "type": "string", @@ -2066,7 +2281,7 @@ "tradable_to": { "type": "string", "x-nullable": true, - "description": "Tradeable to" + "description": "Tradable to" }, "days_until_maturity_min": { "type": "integer", @@ -2142,371 +2357,420 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/seeds/{id}": { - "get": { - "summary": "seeds Detail", - "tags": [ - "seeds" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[seeds]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/plantings": { + "get": { + "summary": "plantings List", + "tags": [ + "plantings" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,slug,planted_at,finished,finished_at,quantity,description,sunniness,planted_from", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[slug]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[crop]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[planted_from]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[garden]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[owner]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "filter[finished]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" - } - }, - "description": "Detail link" - }, - "attributes": { - "type": "object", - "properties": { - "description": { - "type": "string", - "x-nullable": true, - "description": "Description" }, - "quantity": { - "type": "integer", - "x-nullable": true, - "description": "Quanity" - }, - "plant_before": { - "type": "string", - "x-nullable": true, - "description": "Plant before" - }, - "tradable_to": { - "type": "string", - "x-nullable": true, - "description": "Tradable to" - }, - "days_until_maturity_min": { - "type": "integer", - "x-nullable": true, - "description": "Days until maturity (min)" - }, - "days_until_maturity_max": { - "type": "integer", - "x-nullable": true, - "description": "Days until maturity (max)" - }, - "organic": { - "type": "string", - "x-nullable": true, - "description": "Organic" - }, - "gmo": { - "type": "string", - "x-nullable": true, - "description": "GMO" - }, - "heirloom": { - "type": "string", - "x-nullable": true, - "description": "Heirloom" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "owner": { + "attributes": { "type": "object", "properties": { - "links": { + "slug": { + "type": "string", + "x-nullable": true, + "description": "Slug" + }, + "planted_at": { + "type": "string", + "x-nullable": true, + "description": "Planted at" + }, + "finished": { + "type": "boolean", + "x-nullable": false, + "description": "Finished?" + }, + "finished_at": { + "type": "string", + "x-nullable": true, + "description": "Finished at" + }, + "quantity": { + "type": "integer", + "x-nullable": true, + "description": "Quanity" + }, + "description": { + "type": "string", + "x-nullable": true, + "description": "Description" + }, + "sunniness": { + "type": "string", + "x-nullable": true, + "description": "Sunniness" + }, + "planted_from": { + "type": "string", + "x-nullable": true, + "description": "Planted from" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "garden": { "type": "object", "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, "description": "Related link" } }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "crop": { - "type": "object", - "properties": { - "links": { + "description": "Related model" + }, + "crop": { "type": "object", "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, "description": "Related link" } }, - "description": "Related link" + "description": "Related model" + }, + "photos": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" } }, - "description": "Related model" + "description": "Associate data" } - }, - "description": "Associate data" - } + } + }, + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/plantings": { - "get": { - "summary": "plantings List", - "tags": [ - "plantings" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,slug,planted_at,finished,finished_at,quantity,description,sunniness,planted_from", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[slug]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[crop]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[planted_from]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[garden]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[owner]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "filter[finished]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/plantings/{id}": { + "get": { + "summary": "plantings Detail", + "tags": [ + "plantings" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[crops]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -2543,7 +2807,7 @@ "quantity": { "type": "integer", "x-nullable": true, - "description": "Quanity" + "description": "Quantity" }, "description": { "type": "string", @@ -2649,378 +2913,363 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } - } - }, - "/plantings/{id}": { - "get": { - "summary": "plantings Detail", - "tags": [ - "plantings" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[crops]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { + }, + "/photos": { + "get": { + "summary": "photos List", + "tags": [ + "photos" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "page[number]", + "in": "query", + "type": "string", + "description": "Page num", + "required": false + }, + { + "name": "page[size]", + "in": "query", + "type": "string", + "description": "Page size", + "required": false + }, + { + "name": "sort", + "in": "query", + "type": "string", + "description": "Sortable fields: (-)id,thumbnail_url,fullsize_url,license_name,link_url,title", + "required": false + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "filter[id]", + "in": "query", + "type": "string", + "description": "Filter field", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get list", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { "type": "object", "properties": { - "self": { + "id": { "type": "string", + "description": "ID" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Detail link" + } + }, "description": "Detail link" + }, + "attributes": { + "type": "object", + "properties": { + "thumbnail_url": { + "type": "string", + "x-nullable": false, + "description": "Thumbnail URL" + }, + "fullsize_url": { + "type": "string", + "x-nullable": false, + "description": "Full-size URL" + }, + "license_name": { + "type": "string", + "x-nullable": false, + "description": "License name" + }, + "link_url": { + "type": "string", + "x-nullable": false, + "description": "Link URL" + }, + "title": { + "type": "string", + "x-nullable": false, + "description": "Title" + } + }, + "description": "Attributes" + }, + "relationships": { + "type": "object", + "properties": { + "owner": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "plantings": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "gardens": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + }, + "harvests": { + "type": "object", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Associate list link" + }, + "related": { + "type": "string", + "description": "Related link" + } + }, + "description": "Related link" + } + }, + "description": "Related model" + } + }, + "description": "Associate data" } - }, - "description": "Detail link" + } }, - "attributes": { - "type": "object", - "properties": { - "slug": { - "type": "string", - "x-nullable": true, - "description": "Slug" - }, - "planted_at": { - "type": "string", - "x-nullable": true, - "description": "Planted at" - }, - "finished": { - "type": "boolean", - "x-nullable": false, - "description": "Finished?" - }, - "finished_at": { - "type": "string", - "x-nullable": true, - "description": "Finished at" - }, - "quantity": { - "type": "integer", - "x-nullable": true, - "description": "Quantity" - }, - "description": { - "type": "string", - "x-nullable": true, - "description": "Description" - }, - "sunniness": { - "type": "string", - "x-nullable": true, - "description": "Sunniness" - }, - "planted_from": { - "type": "string", - "x-nullable": true, - "description": "Planted from" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "garden": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "crop": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "photos": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } + "description": "Data" }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "meta": { + "type": "object", + "properties": { + "record_count": { + "type": "integer", + "description": "Record count" + }, + "page_count": { + "type": "integer", + "description": "Page count" + } + }, + "description": "Meta" + }, + "links": { + "type": "object", + "properties": { + "first": { + "type": "string", + "description": "First page link" + }, + "next": { + "type": "string", + "description": "Next page link" + }, + "last": { + "type": "string", + "description": "Last page link" + } + }, + "description": "Page links" + } + }, + "required": [ + "data" + ] + } } } } - } - }, - "/photos": { - "get": { - "summary": "photos List", - "tags": [ - "photos" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "page[number]", - "in": "query", - "type": "string", - "description": "Page num", - "required": false - }, - { - "name": "page[size]", - "in": "query", - "type": "string", - "description": "Page size", - "required": false - }, - { - "name": "sort", - "in": "query", - "type": "string", - "description": "Sortable fields: (-)id,thumbnail_url,fullsize_url,license_name,link_url,title", - "required": false - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "filter[id]", - "in": "query", - "type": "string", - "description": "Filter field", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get list", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { + }, + "/photos/{id}": { + "get": { + "summary": "photos Detail", + "tags": [ + "photos" + ], + "produces": [ + "application/vnd.api+json" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID", + "required": true + }, + { + "name": "include", + "in": "query", + "type": "string", + "description": "Include related data", + "required": false + }, + { + "name": "fields[photos]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[members]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[plantings]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[gardens]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + }, + { + "name": "fields[harvests]", + "in": "query", + "type": "string", + "description": "Display field", + "required": false + } + ], + "responses": { + "200": { + "description": "Get detail", + "schema": { + "type": "object", + "properties": { + "data": { "type": "object", "properties": { "id": { "type": "string", "description": "ID" }, + "type": { + "type": "string", + "description": "Type" + }, "links": { "type": "object", "properties": { @@ -3148,266 +3397,17 @@ }, "description": "Associate data" } - } - }, - "description": "Data" + }, + "description": "Data" + } }, - "meta": { - "type": "object", - "properties": { - "record_count": { - "type": "integer", - "description": "Record count" - }, - "page_count": { - "type": "integer", - "description": "Page count" - } - }, - "description": "Meta" - }, - "links": { - "type": "object", - "properties": { - "first": { - "type": "string", - "description": "First page link" - }, - "next": { - "type": "string", - "description": "Next page link" - }, - "last": { - "type": "string", - "description": "Last page link" - } - }, - "description": "Page links" - } - }, - "required": [ - "data" - ] - } - } - } - } - }, - "/photos/{id}": { - "get": { - "summary": "photos Detail", - "tags": [ - "photos" - ], - "produces": [ - "application/vnd.api+json" - ], - "parameters": [ - { - "name": "id", - "in": "path", - "type": "integer", - "description": "ID", - "required": true - }, - { - "name": "include", - "in": "query", - "type": "string", - "description": "Include related data", - "required": false - }, - { - "name": "fields[photos]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[members]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[plantings]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[gardens]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - }, - { - "name": "fields[harvests]", - "in": "query", - "type": "string", - "description": "Display field", - "required": false - } - ], - "responses": { - "200": { - "description": "Get detail", - "schema": { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID" - }, - "type": { - "type": "string", - "description": "Type" - }, - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Detail link" - } - }, - "description": "Detail link" - }, - "attributes": { - "type": "object", - "properties": { - "thumbnail_url": { - "type": "string", - "x-nullable": false, - "description": "Thumbnail URL" - }, - "fullsize_url": { - "type": "string", - "x-nullable": false, - "description": "Full-size URL" - }, - "license_name": { - "type": "string", - "x-nullable": false, - "description": "License name" - }, - "link_url": { - "type": "string", - "x-nullable": false, - "description": "Link URL" - }, - "title": { - "type": "string", - "x-nullable": false, - "description": "Title" - } - }, - "description": "Attributes" - }, - "relationships": { - "type": "object", - "properties": { - "owner": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "plantings": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "gardens": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - }, - "harvests": { - "type": "object", - "properties": { - "links": { - "type": "object", - "properties": { - "self": { - "type": "string", - "description": "Associate list link" - }, - "related": { - "type": "string", - "description": "Related link" - } - }, - "description": "Related link" - } - }, - "description": "Related model" - } - }, - "description": "Associate data" - } - }, - "description": "Data" - } - }, - "required": [ - "data" - ] + "required": [ + "data" + ] + } } } } } } -} -} +} \ No newline at end of file From d95ffdbef621c2cb6bd278f82518f18c7819e706 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 07:38:14 +0000 Subject: [PATCH 052/387] Bump puma from 7.0.2 to 7.0.3 Bumps [puma](https://github.com/puma/puma) from 7.0.2 to 7.0.3. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v7.0.2...v7.0.3) --- updated-dependencies: - dependency-name: puma dependency-version: 7.0.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 41f0f1878..85ddd9d7a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -475,7 +475,7 @@ GEM date stringio public_suffix (6.0.1) - puma (7.0.2) + puma (7.0.3) nio4r (~> 2.0) query_diet (0.7.2) racc (1.8.1) From 64acd4c00c8aa4ecd56f927e655babc24f49c7c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 07:01:15 +0000 Subject: [PATCH 053/387] Bump active_utils from 3.5.0 to 3.6.0 Bumps [active_utils](https://github.com/shopify/active_utils) from 3.5.0 to 3.6.0. - [Release notes](https://github.com/shopify/active_utils/releases) - [Changelog](https://github.com/Shopify/active_utils/blob/main/CHANGELOG.md) - [Commits](https://github.com/shopify/active_utils/compare/v3.5.0...v3.6.0) --- updated-dependencies: - dependency-name: active_utils dependency-version: 3.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 41f0f1878..d74724666 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,7 +84,7 @@ GEM activesupport (>= 7.1) active_record_union (1.3.0) activerecord (>= 4.0) - active_utils (3.5.0) + active_utils (3.6.0) activesupport (>= 4.2) i18n activejob (7.2.2.2) From aa7641ad916f1fd388ba7ccdfecb50b421461607 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Sep 2025 19:11:30 +0000 Subject: [PATCH 054/387] Bump rexml from 3.4.1 to 3.4.2 Bumps [rexml](https://github.com/ruby/rexml) from 3.4.1 to 3.4.2. - [Release notes](https://github.com/ruby/rexml/releases) - [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md) - [Commits](https://github.com/ruby/rexml/compare/v3.4.1...v3.4.2) --- updated-dependencies: - dependency-name: rexml dependency-version: 3.4.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 41f0f1878..f0fec698f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -557,7 +557,7 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.4.1) + rexml (3.4.2) rouge (4.1.2) rspec (3.13.0) rspec-core (~> 3.13.0) From 600e61a28269e0c4c263a77080e189f99370e411 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 07:01:16 +0000 Subject: [PATCH 055/387] Bump recaptcha from 5.20.1 to 5.21.1 Bumps [recaptcha](https://github.com/ambethia/recaptcha) from 5.20.1 to 5.21.1. - [Changelog](https://github.com/ambethia/recaptcha/blob/master/CHANGELOG.md) - [Commits](https://github.com/ambethia/recaptcha/compare/v5.20.1...v5.21.1) --- updated-dependencies: - dependency-name: recaptcha dependency-version: 5.21.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 41f0f1878..2de53f1cd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -543,7 +543,7 @@ GEM rdoc (6.14.2) erb psych (>= 4.0.0) - recaptcha (5.20.1) + recaptcha (5.21.1) redis-client (0.23.2) connection_pool regexp_parser (2.11.2) From ce7ce70d5f85f517f74da1158ce2bbd2c0534f8f Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sat, 20 Sep 2025 17:49:47 +0930 Subject: [PATCH 056/387] Merge pull request #4222 from Growstuff/feature/add-activity-prompts feat: Add prompts for new activities --- app/controllers/activities_controller.rb | 14 +++++++++++++- app/controllers/gardens_controller.rb | 5 ++++- app/controllers/plantings_controller.rb | 7 ++++++- config/locales/en.yml | 3 +++ spec/features/plantings/planting_a_crop_spec.rb | 2 +- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb index 8ca7f956e..3cb447def 100644 --- a/app/controllers/activities_controller.rb +++ b/app/controllers/activities_controller.rb @@ -32,6 +32,8 @@ class ActivitiesController < DataController owner: current_member, due_date: Date.today ) + @activity.name = params[:name] if params[:name] + @activity.due_date = params[:due_date] if params[:due_date] if params[:garden_id] @activity.garden = Garden.find_by( owner: current_member, @@ -63,7 +65,17 @@ class ActivitiesController < DataController end def update - @activity.update(activity_params) + if @activity.update(activity_params) + if activity_params[:finished].present? + link = new_activity_path( + name: @activity.name, + garden_id: @activity.garden_id, + planting_id: @activity.planting_id, + due_date: 2.weeks.from_now.to_date + ) + flash[:notice] = t('activities.finished_prompt_html', link: link).html_safe + end + end respond_with @activity end diff --git a/app/controllers/gardens_controller.rb b/app/controllers/gardens_controller.rb index 3d50dfa03..4f0bbd6aa 100644 --- a/app/controllers/gardens_controller.rb +++ b/app/controllers/gardens_controller.rb @@ -39,7 +39,10 @@ class GardensController < DataController def create @garden.owner_id = current_member.id - flash[:notice] = I18n.t('gardens.created') if @garden.save + if @garden.save + link = new_activity_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 diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index c123fb869..8e6228163 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -83,7 +83,12 @@ class PlantingsController < DataController end def update - @planting.update(planting_params) + if @planting.update(planting_params) + if planting_params[:finished].present? && @planting.garden.plantings.current.empty? + link = new_activity_path(name: 'Cultivate soil', garden_id: @planting.garden_id) + flash[:notice] = t('plantings.finished_prompt_html', link: link).html_safe + end + end respond_with @planting end diff --git a/config/locales/en.yml b/config/locales/en.yml index 0f37ed647..d0c1adc4d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -125,6 +125,7 @@ en: updated: Garden was successfully updated. confirm_delete: All plantings associated with this garden will also be deleted. Are you sure? confirm_deactivate: All plantings associated with this garden will be marked as finished. Are you sure? + created_prompt_html: "Garden was successfully created. Would you like to plan to weed this garden bed in two weeks?" harvests: created: Harvest was successfully created. harvest_something: Harvest something @@ -301,6 +302,7 @@ en: finish_helper: > An activity is finished when you've completed it, or it's otherwise no longer possible. + finished_prompt_html: "Activity finished. Would you like to repeat this activity in two weeks?" plantings: badges: days_until_finished: days until finished @@ -325,6 +327,7 @@ en: string: "%{crop} planting in %{garden} by %{owner}" progress: progress_0_not_planted_yet: 'Progress: 0% - not planted yet' + finished_prompt_html: "Planting was successfully updated. Would you like to plan a soil cultivation activity?" posts: write_blog_post: Write blog post index: diff --git a/spec/features/plantings/planting_a_crop_spec.rb b/spec/features/plantings/planting_a_crop_spec.rb index 4aa8a243b..e171e2244 100644 --- a/spec/features/plantings/planting_a_crop_spec.rb +++ b/spec/features/plantings/planting_a_crop_spec.rb @@ -187,7 +187,7 @@ describe "Planting a crop", :js, :search do check "finished" fill_in "Finished date", with: "2015-06-25" click_button "Save" - expect(page).to have_content "planting was successfully updated" + expect(page).to have_content "Planting was successfully updated" expect(page).to have_content "Finished" end From 5db6a86607b074a7c6f62cba19576eaf403de70d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 08:54:12 +0000 Subject: [PATCH 057/387] Add ability to copy/duplicate an activity --- app/controllers/activities_controller.rb | 2 ++ app/helpers/buttons_helper.rb | 13 +++++++++++++ app/helpers/icons_helper.rb | 4 ++++ app/views/activities/_actions.haml | 1 + config/locales/en.yml | 1 + 5 files changed, 21 insertions(+) diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb index 3cb447def..3d72ed439 100644 --- a/app/controllers/activities_controller.rb +++ b/app/controllers/activities_controller.rb @@ -33,6 +33,8 @@ class ActivitiesController < DataController due_date: Date.today ) @activity.name = params[:name] if params[:name] + @activity.description = params[:description] if params[:description] + @activity.category = params[:category] if params[:category] @activity.due_date = params[:due_date] if params[:due_date] if params[:garden_id] @activity.garden = Garden.find_by( diff --git a/app/helpers/buttons_helper.rb b/app/helpers/buttons_helper.rb index ac929f711..2d214dea2 100644 --- a/app/helpers/buttons_helper.rb +++ b/app/helpers/buttons_helper.rb @@ -88,6 +88,19 @@ module ButtonsHelper edit_button(edit_activity_path(activity), classes:) end + def activity_copy_button(activity, classes: 'btn') + link_to new_activity_path( + name: activity.name, + description: activity.description, + category: activity.category, + garden_id: activity.garden_id, + planting_id: activity.planting_id, + due_date: activity.due_date + ), class: classes do + copy_icon + ' ' + t('buttons.copy') + end + end + def activity_finish_button(activity, classes: 'btn btn-default btn-secondary') return unless can?(:edit, activity) || activity.finished diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 71721ef04..80c118d00 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -59,6 +59,10 @@ module IconsHelper image_icon 'delete' end + def copy_icon + icon('far', 'copy') + end + def add_photo_icon image_icon 'add-photo' end diff --git a/app/views/activities/_actions.haml b/app/views/activities/_actions.haml index 1631b7042..c87a01625 100644 --- a/app/views/activities/_actions.haml +++ b/app/views/activities/_actions.haml @@ -3,6 +3,7 @@ %a#activity-actions-button.btn.btn-info.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", type: "button", href: '#'} Actions .dropdown-menu.dropdown-menu-xs{"aria-labelledby" => "planting-actions-button"} = activity_edit_button(activity, classes: 'dropdown-item') + = activity_copy_button(activity, classes: 'dropdown-item') - if activity.active = activity_finish_button(activity, classes: 'dropdown-item') .dropdown-divider diff --git a/config/locales/en.yml b/config/locales/en.yml index d0c1adc4d..56680c272 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -72,6 +72,7 @@ en: add: Add add_photo: Add photo add_seed_to_stash: Add %{crop_name} seeds to stash + copy: Copy delete: Delete edit: Edit harvest: Harvest From 54acc369abc910e90d47cae8c75cdb498fec86c2 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sat, 20 Sep 2025 09:43:22 +0000 Subject: [PATCH 058/387] Rubocop --- app/controllers/activities_controller.rb | 18 ++++++++---------- app/controllers/plantings_controller.rb | 8 +++----- app/helpers/auto_suggest_helper.rb | 2 +- app/helpers/event_helper.rb | 4 ++-- app/models/forum.rb | 1 + app/models/garden_type.rb | 1 + app/models/member.rb | 9 ++++++--- app/models/plant_part.rb | 1 + app/models/role.rb | 1 + app/models/seed.rb | 3 ++- app/resources/base_resource.rb | 2 +- 11 files changed, 27 insertions(+), 23 deletions(-) diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb index 3d72ed439..84f676873 100644 --- a/app/controllers/activities_controller.rb +++ b/app/controllers/activities_controller.rb @@ -67,16 +67,14 @@ class ActivitiesController < DataController end def update - if @activity.update(activity_params) - if activity_params[:finished].present? - link = new_activity_path( - name: @activity.name, - garden_id: @activity.garden_id, - planting_id: @activity.planting_id, - due_date: 2.weeks.from_now.to_date - ) - flash[:notice] = t('activities.finished_prompt_html', link: link).html_safe - end + if @activity.update(activity_params) && activity_params[:finished].present? + link = new_activity_path( + name: @activity.name, + garden_id: @activity.garden_id, + planting_id: @activity.planting_id, + due_date: 2.weeks.from_now.to_date + ) + flash[:notice] = t('activities.finished_prompt_html', link: link).html_safe end respond_with @activity end diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index 8e6228163..78cfc88d3 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -83,11 +83,9 @@ class PlantingsController < DataController end def update - if @planting.update(planting_params) - if planting_params[:finished].present? && @planting.garden.plantings.current.empty? - link = new_activity_path(name: 'Cultivate soil', garden_id: @planting.garden_id) - flash[:notice] = t('plantings.finished_prompt_html', link: link).html_safe - end + if @planting.update(planting_params) && planting_params[:finished].present? && @planting.garden.plantings.current.empty? + link = new_activity_path(name: 'Cultivate soil', garden_id: @planting.garden_id) + flash[:notice] = t('plantings.finished_prompt_html', link: link).html_safe end respond_with @planting end diff --git a/app/helpers/auto_suggest_helper.rb b/app/helpers/auto_suggest_helper.rb index ff0558ef2..9cc75107d 100644 --- a/app/helpers/auto_suggest_helper.rb +++ b/app/helpers/auto_suggest_helper.rb @@ -13,7 +13,7 @@ module AutoSuggestHelper resource = resource.class.name.downcase source_path = Rails.application.routes.url_helpers.send("search_#{source}s_path", format: :json) %( -