From 98c8bdc0bb41071154d49cbcb7846adddbf2f50e Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 27 Apr 2026 00:37:51 +0930 Subject: [PATCH 01/42] Merge pull request #4564 from Growstuff/memory-optimization-2149092598558110155 Memory usage optimization --- app/views/alternate_names/_form.html.haml | 4 ++-- app/views/crops/_form.html.haml | 2 +- app/views/scientific_names/_form.html.haml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/alternate_names/_form.html.haml b/app/views/alternate_names/_form.html.haml index dab8a7c1d..fdfe71a63 100644 --- a/app/views/alternate_names/_form.html.haml +++ b/app/views/alternate_names/_form.html.haml @@ -22,8 +22,8 @@ .form-group = f.label :crop_id, class: 'control-label col-md-2' .col-md-8 - = collection_select(:alternate_name, :crop_id, - Crop.all, :id, :name, + = select(:alternate_name, :crop_id, + Crop.order(:name).pluck(:name, :id), { selected: @alternate_name.crop_id || @crop.id }, class: 'form-control') diff --git a/app/views/crops/_form.html.haml b/app/views/crops/_form.html.haml index 6fc3849c4..8ada38479 100644 --- a/app/views/crops/_form.html.haml +++ b/app/views/crops/_form.html.haml @@ -85,7 +85,7 @@ -# Only crop wranglers see the crop hierarchy (for now) - if can? :wrangle, @crop - = f.collection_select(:parent_id, Crop.all.order(:name), :id, :name, + = f.select(:parent_id, Crop.order(:name).pluck(:name, :id), { include_blank: true, label: 'Parent crop'}) %span.help-block Optional. For setting up crop hierarchies for varieties etc. diff --git a/app/views/scientific_names/_form.html.haml b/app/views/scientific_names/_form.html.haml index 912f40135..4769bce42 100644 --- a/app/views/scientific_names/_form.html.haml +++ b/app/views/scientific_names/_form.html.haml @@ -17,8 +17,8 @@ .form-group = f.label :crop_id, class: 'control-label col-md-2' .col-md-8 - = collection_select(:scientific_name, :crop_id, Crop.all.order(:name), :id, - :name, { selected: @scientific_name.crop_id || @crop.id }, + = select(:scientific_name, :crop_id, Crop.order(:name).pluck(:name, :id), + { selected: @scientific_name.crop_id || @crop.id }, class: 'form-control') .form-group = f.label :name, class: 'control-label col-md-2' From 2723599f276d44799552a5df3f3e53f705b73874 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sun, 26 Apr 2026 16:07:29 +0000 Subject: [PATCH 02/42] Add fragment cache for crop partials --- app/views/crops/show.html.haml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index 0cde256ea..777d3d75d 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -73,10 +73,11 @@ = pie_chart crop_harvested_for_path(@crop, format: :json), legend: "bottom" - if @crop.varieties.any? - %section.varieties - %h2 Varieties - .index-cards - = render 'varieties', crop: @crop + - cache [@crop, 'varieties'] do + %section.varieties + %h2 Varieties + .index-cards + = render 'varieties', crop: @crop %section.crop-map %h2 @@ -134,9 +135,11 @@ = render 'harvests', crop: @crop = render 'find_seeds', crop: @crop - = render 'openfarm_data', crop: @crop + - cache [@crop, 'openfarm_data'] do + = render 'openfarm_data', crop: @crop - = render 'nutritional_data', crop: @crop + - cache [@crop, 'nutritional_data'] do + = render 'nutritional_data', crop: @crop = cute_icon .card From 8e7dd25e9831974ca19dd6f6bba1060730a75427 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 27 Apr 2026 01:40:54 +0930 Subject: [PATCH 03/42] Add rake task to cleanup inactive members (#4574) * Add members:cleanup_inactive rake task This task identifies and deletes members who have not logged in for over 24 months and have no gardens, plantings, or other activity (posts, comments, seeds, harvests, etc). Includes support for DRY_RUN=true to preview deletions. Added tests in spec/tasks/members_spec.rb. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> * Refactor activity check to Member#has_activity? and update rake task - Added `Member#has_activity?` to encapsulate the check for gardens, plantings, and other activity. - Updated `members:cleanup_inactive` rake task to use `Member#has_activity?`. - Maintained `DRY_RUN` support and existing tests. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> * Apply suggestion from @CloCkWeRX * Apply suggestions from code review Co-authored-by: Daniel O'Connor --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- app/models/member.rb | 21 ++++++++++++ lib/tasks/members.rake | 33 ++++++++++++++++++ spec/tasks/members_spec.rb | 68 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 lib/tasks/members.rake create mode 100644 spec/tasks/members_spec.rb diff --git a/app/models/member.rb b/app/models/member.rb index be7343985..bcb0a5e62 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -196,4 +196,25 @@ class Member < ApplicationRecord def get_block(member) blocks.find_by(blocked_id: member.id) if already_blocking?(member) end + + def has_activity? + gardens.exists? || + plantings.exists? || + harvests.exists? || + seeds.exists? || + photos.exists? || + forums.exists? || + activities.exists? || + posts.exists? || + comments.exists? || + requested_crops.exists? || + created_crops.exists? || + likes.exists? || + created_alternate_names.exists? || + created_scientific_names.exists? || + follows.exists? || + inverse_follows.exists? || + blocks.exists? || + inverse_blocks.exists? + end end diff --git a/lib/tasks/members.rake b/lib/tasks/members.rake new file mode 100644 index 000000000..c315fa884 --- /dev/null +++ b/lib/tasks/members.rake @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +namespace :members do + desc "Remove inactive members with no activity and last login > 24 months ago" + # usage: rake members:cleanup_inactive + # usage: DRY_RUN=true rake members:cleanup_inactive + task cleanup_inactive: :environment do + limit_date = 24.months.ago + dry_run = ENV.fetch('DRY_RUN', 'false') == 'true' + + inactive_members = Member.where("last_sign_in_at < ? OR (last_sign_in_at IS NULL AND created_at < ?)", limit_date, limit_date).limit(10) + + count = 0 + inactive_members.find_each do |member| + # Check for activity using the model method + unless member.has_activity? + if dry_run + puts "[DRY RUN] Would delete inactive member: #{member.login_name} (ID: #{member.id}, Last login: #{member.last_sign_in_at || 'Never'}, Created: #{member.created_at})" + else + puts "Deleting inactive member: #{member.login_name} (ID: #{member.id}, Last login: #{member.last_sign_in_at || 'Never'}, Created: #{member.created_at})" + member.destroy + end + count += 1 + end + end + + if dry_run + puts "Total inactive members that would be deleted: #{count}" + else + puts "Total inactive members deleted: #{count}" + end + end +end diff --git a/spec/tasks/members_spec.rb b/spec/tasks/members_spec.rb new file mode 100644 index 000000000..d4be70207 --- /dev/null +++ b/spec/tasks/members_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'rake' + +describe 'members:cleanup_inactive' do + before :all do + Rails.application.load_tasks + end + + let(:cleanup_task) { Rake::Task['members:cleanup_inactive'] } + + before do + cleanup_task.reenable + end + + it "deletes inactive members with no gardens and no other activity" do + inactive_no_activity = create(:member, last_sign_in_at: 25.months.ago) + + # We must explicitly remove the default garden to test the "no gardens" condition + inactive_no_activity.gardens.destroy_all + expect(inactive_no_activity.gardens.count).to eq(0) + + cleanup_task.invoke + + expect(Member.exists?(inactive_no_activity.id)).to be_falsey + end + + it "does not delete inactive members with a garden (even if empty)" do + inactive_with_garden = create(:member, last_sign_in_at: 25.months.ago) + # They have 1 default garden + expect(inactive_with_garden.gardens.count).to eq(1) + + cleanup_task.invoke + + expect(Member.exists?(inactive_with_garden.id)).to be_truthy + end + + it "does not delete members with recent login" do + recent_member = create(:member, last_sign_in_at: 1.month.ago) + recent_member.gardens.destroy_all + + cleanup_task.invoke + + expect(Member.exists?(recent_member.id)).to be_truthy + end + + it "does not delete inactive members with activity (posts)" do + inactive_with_post = create(:member, last_sign_in_at: 25.months.ago) + inactive_with_post.gardens.destroy_all + create(:post, author: inactive_with_post) + + cleanup_task.invoke + + expect(Member.exists?(inactive_with_post.id)).to be_truthy + end + + it "honors DRY_RUN environment variable" do + inactive_no_activity = create(:member, last_sign_in_at: 25.months.ago) + inactive_no_activity.gardens.destroy_all + + ENV['DRY_RUN'] = 'true' + cleanup_task.invoke + ENV['DRY_RUN'] = nil + + expect(Member.exists?(inactive_no_activity.id)).to be_truthy + end +end From 3127f45d0f2a34fd0df681035806a4097221eacc Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 27 Apr 2026 02:15:17 +0930 Subject: [PATCH 04/42] Merge pull request #4578 from Growstuff/member-inactive-delete Delete inactive members with no activity in 3 years --- app/models/member.rb | 2 +- lib/tasks/members.rake | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/member.rb b/app/models/member.rb index bcb0a5e62..f7eb7b326 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -198,7 +198,7 @@ class Member < ApplicationRecord end def has_activity? - gardens.exists? || + (gardens.exists? && gardens.count > 1) || plantings.exists? || harvests.exists? || seeds.exists? || diff --git a/lib/tasks/members.rake b/lib/tasks/members.rake index c315fa884..bd68b4b17 100644 --- a/lib/tasks/members.rake +++ b/lib/tasks/members.rake @@ -5,10 +5,10 @@ namespace :members do # usage: rake members:cleanup_inactive # usage: DRY_RUN=true rake members:cleanup_inactive task cleanup_inactive: :environment do - limit_date = 24.months.ago + limit_date = 3.years.ago dry_run = ENV.fetch('DRY_RUN', 'false') == 'true' - inactive_members = Member.where("last_sign_in_at < ? OR (last_sign_in_at IS NULL AND created_at < ?)", limit_date, limit_date).limit(10) + inactive_members = Member.where("last_sign_in_at < ? OR (last_sign_in_at IS NULL AND created_at < ?)", limit_date, limit_date) count = 0 inactive_members.find_each do |member| From 2e56f8cb2fd428fe8295c2b1f6b0c44c296fd5ee Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 27 Apr 2026 03:52:11 +0000 Subject: [PATCH 05/42] Cache what a crop is harvested for --- app/controllers/charts/crops_controller.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/controllers/charts/crops_controller.rb b/app/controllers/charts/crops_controller.rb index a48ec1ab2..848c7161d 100644 --- a/app/controllers/charts/crops_controller.rb +++ b/app/controllers/charts/crops_controller.rb @@ -14,9 +14,12 @@ module Charts def harvested_for @crop = Crop.find_by!(slug: params[:crop_slug]) - render json: Harvest.joins(:plant_part) - .where(crop: @crop) - .group("plant_parts.name").count(:id) + data = Rails.cache.fetch("#{@crop.cache_key_with_version}/harvested_for", expires_in: 1.day) do + Harvest.joins(:plant_part) + .where(crop: @crop) + .group("plant_parts.name").count(:id) + end + render json: data end private From 9abb0d02b9618687f8d1b120c5f6e01180c5b26a Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 27 Apr 2026 13:23:17 +0930 Subject: [PATCH 06/42] Merge pull request #4581 from Growstuff/add-rack-attack-protection-3014929071908440304 Add Rack::Attack rate limiting and Fail2Ban protection --- Gemfile | 2 ++ Gemfile.lock | 3 +++ config/application.rb | 2 ++ config/initializers/rack_attack.rb | 34 ++++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 config/initializers/rack_attack.rb diff --git a/Gemfile b/Gemfile index 4c8e67b8f..6d9a825cb 100644 --- a/Gemfile +++ b/Gemfile @@ -116,6 +116,8 @@ gem 'xmlrpc' # fixes rake error - can be removed if not needed later gem 'puma' +gem 'rack-attack' + gem 'loofah', '>= 2.19.1' gem 'rack-protection', '>= 2.0.1' diff --git a/Gemfile.lock b/Gemfile.lock index f610150fe..fc136b666 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -503,6 +503,8 @@ GEM query_diet (0.7.3) racc (1.8.1) rack (2.2.23) + rack-attack (6.8.0) + rack (>= 1.0, < 4) rack-cors (2.0.2) rack (>= 2.0.0) rack-protection (3.2.0) @@ -841,6 +843,7 @@ DEPENDENCIES pry puma query_diet + rack-attack rack-cors rack-protection (>= 2.0.1) rails (~> 7.2.0) diff --git a/config/application.rb b/config/application.rb index ff16123ee..a8b0dbcfb 100644 --- a/config/application.rb +++ b/config/application.rb @@ -73,6 +73,8 @@ module Growstuff config.newsletter_list_id = ENV.fetch('GROWSTUFF_MAILCHIMP_NEWSLETTER_ID', nil) # config.active_record.raise_in_transactional_callbacks = true + config.middleware.insert_before 0, Rack::Attack + config.middleware.insert_before 0, Rack::Cors do allow do origins '*' diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb new file mode 100644 index 000000000..2d1e40e22 --- /dev/null +++ b/config/initializers/rack_attack.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class Rack::Attack + ### Throttle Config ### + + if Rails.env.production? + # Throttle requests to /plantings, /harvests, and /members to 10 per minute per IP + # Includes API routes + throttle('req/ip/restricted_routes', limit: 20, period: 1.minute) do |req| + if req.path =~ %r{^/(plantings|harvests|members)(/|$)} || req.path =~ %r{^/api/v1/(plantings|harvests|members)(/|$)} + req.ip + end + end + + ### Fail2Ban Config ### + + # Block IPs that make too many requests to suspicious paths + # After 5 "bad" requests in 10 minutes, block the IP for 1 hour + blocklist('fail2ban/pentesters') do |req| + Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 5, findtime: 10.minutes, bantime: 1.hour) do + # The count for the IP is incremented if the return value is truthy. + req.path.include?('wp-admin') || + req.path.include?('wp-login') || + req.path.include?('cgi-bin') || + req.path.end_with?('.php', '.asp', '.aspx', '.jsp', '.exe', '.env', '.git') + end + end + end + + ### Custom Response Headers ### + + # Add Retry-After header to throttled responses + self.throttled_response_retry_after_header = true +end From 37e9860fdf1e9a2056db663f156c6be9ea2dd26d Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 27 Apr 2026 14:19:50 +0930 Subject: [PATCH 07/42] Update member_slug lookup to 404 when not found (#4584) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- app/controllers/activities_controller.rb | 6 +++--- app/controllers/gardens_controller.rb | 2 +- app/controllers/harvests_controller.rb | 4 ++-- app/controllers/plantings_controller.rb | 6 +++--- app/controllers/posts_controller.rb | 2 +- app/controllers/seeds_controller.rb | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb index ae489df98..86b22bf4f 100644 --- a/app/controllers/activities_controller.rb +++ b/app/controllers/activities_controller.rb @@ -7,9 +7,9 @@ class ActivitiesController < DataController where = {} where['active'] = true unless @show_all - if params[:member_slug] - @owner = Member.find_by(slug: params[:member_slug]) - where['owner_id'] = @owner.id unless @owner.nil? + if params[:member_slug].present? + @owner = Member.find_by!(slug: params[:member_slug]) + where['owner_id'] = @owner.id end @activities = Activity.search( diff --git a/app/controllers/gardens_controller.rb b/app/controllers/gardens_controller.rb index 4f0bbd6aa..d28fab854 100644 --- a/app/controllers/gardens_controller.rb +++ b/app/controllers/gardens_controller.rb @@ -2,7 +2,7 @@ class GardensController < DataController def index - @owner = Member.find_by(slug: params[:member_slug]) + @owner = Member.find_by!(slug: params[:member_slug]) if params[:member_slug].present? @show_all = params[:all] == '1' @show_jump_to = params[:member_slug].present? || false diff --git a/app/controllers/harvests_controller.rb b/app/controllers/harvests_controller.rb index 6db460e69..3b003c716 100644 --- a/app/controllers/harvests_controller.rb +++ b/app/controllers/harvests_controller.rb @@ -5,8 +5,8 @@ class HarvestsController < DataController def index where = {} - if params[:member_slug] - @owner = Member.find_by(slug: params[:member_slug]) + if params[:member_slug].present? + @owner = Member.find_by!(slug: params[:member_slug]) where['owner_id'] = @owner.id end diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index bc443fe8e..d903a25da 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -11,9 +11,9 @@ class PlantingsController < DataController where = {} where['active'] = true unless @show_all - if params[:member_slug] - @owner = Member.find_by(slug: params[:member_slug]) - where['owner_id'] = @owner.id unless @owner.nil? + if params[:member_slug].present? + @owner = Member.find_by!(slug: params[:member_slug]) + where['owner_id'] = @owner.id end if params[:crop_slug] diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 01239a601..4b24953fb 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -8,7 +8,7 @@ class PostsController < ApplicationController respond_to :rss, only: %i(index show) def index - @author = Member.find_by(slug: params[:member_slug]) + @author = Member.find_by!(slug: params[:member_slug]) if params[:member_slug].present? @posts = posts respond_with(@posts) end diff --git a/app/controllers/seeds_controller.rb b/app/controllers/seeds_controller.rb index 46e00c0cc..897f34710 100644 --- a/app/controllers/seeds_controller.rb +++ b/app/controllers/seeds_controller.rb @@ -5,7 +5,7 @@ class SeedsController < DataController where = {} if params[:member_slug].present? - @owner = Member.find_by(slug: params[:member_slug]) + @owner = Member.find_by!(slug: params[:member_slug]) where['owner_id'] = @owner.id end From 1eac00705edfc77fbb28a30e622d3d356418b690 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:25:50 +0000 Subject: [PATCH 08/42] Bump actions/checkout from 4 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-build-push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 8e8b8bd3b..75d09c969 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 From 9fe1fddac17c92ead4c6c7a215a47ac70b50a3d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:26:19 +0000 Subject: [PATCH 09/42] Bump puma from 8.0.0 to 8.0.1 Bumps [puma](https://github.com/puma/puma) from 8.0.0 to 8.0.1. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/main/History.md) - [Commits](https://github.com/puma/puma/compare/v8.0.0...v8.0.1) --- updated-dependencies: - dependency-name: puma dependency-version: 8.0.1 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 fc136b666..1bc3d997d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -498,7 +498,7 @@ GEM date stringio public_suffix (7.0.5) - puma (8.0.0) + puma (8.0.1) nio4r (~> 2.0) query_diet (0.7.3) racc (1.8.1) From 1b4b8f94d188622b72cd53bf93038da650725304 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:42:38 +0000 Subject: [PATCH 10/42] Bump docker/metadata-action from 5 to 6 Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/v5...v6) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-build-push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 75d09c969..edbefcec8 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -28,7 +28,7 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ghcr.io/${{ github.repository }} From ff9d99afe52d8c6d943903b008845eed9e5b0634 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:46:08 +0000 Subject: [PATCH 11/42] Improve Charts::CropsController with caching and refactoring - Added Rails.cache.fetch to `sunniness` and `planted_from` actions. - Refactored crop loading into a `before_action :set_crop`. - Updated specs to verify caching behavior and ensure coverage. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> --- app/controllers/charts/crops_controller.rb | 18 ++++++---- .../charts/crops_controller_spec.rb | 33 +++++++++++++++---- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/app/controllers/charts/crops_controller.rb b/app/controllers/charts/crops_controller.rb index 848c7161d..f8f87af10 100644 --- a/app/controllers/charts/crops_controller.rb +++ b/app/controllers/charts/crops_controller.rb @@ -3,6 +3,7 @@ module Charts class CropsController < ApplicationController respond_to :json + before_action :set_crop def sunniness pie_chart_query 'sunniness' @@ -13,7 +14,6 @@ module Charts end def harvested_for - @crop = Crop.find_by!(slug: params[:crop_slug]) data = Rails.cache.fetch("#{@crop.cache_key_with_version}/harvested_for", expires_in: 1.day) do Harvest.joins(:plant_part) .where(crop: @crop) @@ -24,12 +24,18 @@ module Charts private - def pie_chart_query(field) + def set_crop @crop = Crop.find_by!(slug: params[:crop_slug]) - render json: Planting.where(crop: @crop) - .where.not(field.to_sym => nil) - .where.not(field.to_sym => '') - .group(field.to_sym).count(:id) + end + + def pie_chart_query(field) + data = Rails.cache.fetch("#{@crop.cache_key_with_version}/#{field}", expires_in: 1.day) do + Planting.where(crop: @crop) + .where.not(field.to_sym => nil) + .where.not(field.to_sym => '') + .group(field.to_sym).count(:id) + end + render json: data end end end diff --git a/spec/controllers/charts/crops_controller_spec.rb b/spec/controllers/charts/crops_controller_spec.rb index 9de323e3b..99ede5b02 100644 --- a/spec/controllers/charts/crops_controller_spec.rb +++ b/spec/controllers/charts/crops_controller_spec.rb @@ -7,21 +7,42 @@ describe Charts::CropsController do let(:crop) { create(:crop) } describe 'sunniness' do - before { get :sunniness, params: { crop_slug: crop.to_param } } + it "returns a successful response" do + get :sunniness, params: { crop_slug: crop.to_param } + expect(response).to be_successful + end - it { expect(response).to be_successful } + it "caches the result" do + cache_key = "#{crop.cache_key_with_version}/sunniness" + expect(Rails.cache).to receive(:fetch).with(cache_key, expires_in: 1.day).and_call_original + get :sunniness, params: { crop_slug: crop.to_param } + end end describe 'planted_from' do - before { get :planted_from, params: { crop_slug: crop.to_param } } + it "returns a successful response" do + get :planted_from, params: { crop_slug: crop.to_param } + expect(response).to be_successful + end - it { expect(response).to be_successful } + it "caches the result" do + cache_key = "#{crop.cache_key_with_version}/planted_from" + expect(Rails.cache).to receive(:fetch).with(cache_key, expires_in: 1.day).and_call_original + get :planted_from, params: { crop_slug: crop.to_param } + end end describe 'harvested_for' do - before { get :harvested_for, params: { crop_slug: crop.to_param } } + it "returns a successful response" do + get :harvested_for, params: { crop_slug: crop.to_param } + expect(response).to be_successful + end - it { expect(response).to be_successful } + it "caches the result" do + cache_key = "#{crop.cache_key_with_version}/harvested_for" + expect(Rails.cache).to receive(:fetch).with(cache_key, expires_in: 1.day).and_call_original + get :harvested_for, params: { crop_slug: crop.to_param } + end end end end From a2bb6c71621f03e3589a97054a061da699ca97d8 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 27 Apr 2026 17:19:31 +0930 Subject: [PATCH 12/42] Ban Semrush --- public/robots.txt | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/public/robots.txt b/public/robots.txt index 61e736fd1..eb0c67cc8 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -123,13 +123,27 @@ Disallow: / User-agent: WebReaper Disallow: / -# Per their statement, semrushbot respects crawl-delay directives -# We want them to overall stay within reasonable request rates to -# the backend (20 rps); keeping in mind that the crawl-delay will -# be applied by site and not globally by the bot, 5 seconds seem -# like a reasonable approximation +# Semrush seem to crawl everything. User-agent: SemrushBot -Crawl-delay: 5 +Disallow: / + +User-agent: SiteAuditBot +Disallow: / + +User-agent: SemrushBot-BA +Disallow: / + +User-agent: SemrushBot-SI +Disallow: / + +User-agent: SemrushBot-SWA +Disallow: / + +User-agent: SplitSignalBot +Disallow: / + +User-agent: SemrushBot-OCOB +Disallow: / # # Friendly, low-speed bots are welcome viewing pages, but not From 5a462bd74006f3455a001a347ef4d2c068b7d664 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 07:57:06 +0000 Subject: [PATCH 13/42] Bump docker/setup-buildx-action from 3 to 4 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-build-push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index edbefcec8..79b9ae7b8 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Log in to the Container registry uses: docker/login-action@v3 From dcd701fe9ddf8004293349356c8f9b0191956fd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 08:00:50 +0000 Subject: [PATCH 14/42] Bump docker/build-push-action from 5 to 7 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 7. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v5...v7) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-build-push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index edbefcec8..126819915 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -33,7 +33,7 @@ jobs: images: ghcr.io/${{ github.repository }} - name: Build and push Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . push: true From 91842853883aa7d471eee687994ef91cf5b45cc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 08:00:55 +0000 Subject: [PATCH 15/42] Bump docker/login-action from 3 to 4 Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docker-build-push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index edbefcec8..928063231 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -20,7 +20,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Log in to the Container registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} From 464017de6ffab388b1cdf5dd45887da2989f979d Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Mon, 27 Apr 2026 08:05:32 +0000 Subject: [PATCH 16/42] Try planting filtering --- app/controllers/photos_controller.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index a65f9ec05..a10bb430d 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -118,6 +118,8 @@ class PhotosController < ApplicationController { crops: @crop.id } elsif params[:planting_id] { planting_id: @planting.id } + elsif params[:planting_slug] + { plantings: @planting.id } else {} end @@ -126,5 +128,6 @@ class PhotosController < ApplicationController def set_crop_and_planting @crop = Crop.find params[:crop_slug] if params[:crop_slug] @planting = Planting.find params[:planting_id] if params[:planting_id] + @planting ||= Planting.find params[:planting_slug] if params[:planting_slug] end end From aa0ee65d78d1fcc3621c73260b5925fdeb90e413 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:55:46 +0000 Subject: [PATCH 17/42] Optimize CropsHelper with caching and memoization - Implement instance-level memoization for `crop_or_parent` and `display_seed_availability` - Use `Rails.cache.fetch` for `crop_jsonld_data` to improve performance of JSON-LD generation - Optimize `display_seed_availability` to avoid redundant queries - Fix a potential `NameError` in `crop_jsonld_data` by initializing `images` properly - Ensure memoization keys handle non-persisted objects and nil results correctly Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> --- app/helpers/crops_helper.rb | 139 +++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 58 deletions(-) diff --git a/app/helpers/crops_helper.rb b/app/helpers/crops_helper.rb index e331a1d05..1ac4c626e 100644 --- a/app/helpers/crops_helper.rb +++ b/app/helpers/crops_helper.rb @@ -2,28 +2,47 @@ module CropsHelper def crop_or_parent(crop, attribute) - default = crop.send(attribute) - return default if default.present? + @crop_or_parent_cache ||= {} + cache_key = [crop.persisted? ? crop.id : crop.object_id, attribute] + return @crop_or_parent_cache[cache_key] if @crop_or_parent_cache.key?(cache_key) - parent = crop - while parent = parent.parent - return parent.send(attribute) if parent&.send(attribute).present? + @crop_or_parent_cache[cache_key] = begin + value = crop.send(attribute) + if value.blank? + parent = crop + while (parent = parent.parent) + parent_value = parent.send(attribute) + if parent_value.present? + value = parent_value + break + end + end + end + value end - - # For scopes, arrays, etc return the empty value - default end def display_seed_availability(member, crop) - seeds = member.seeds.where(crop:) - total_quantity = seeds.where.not(quantity: nil).sum(:quantity) + @seed_availability_cache ||= {} + cache_key = [ + member.persisted? ? member.id : member.object_id, + crop.persisted? ? crop.id : crop.object_id + ] + return @seed_availability_cache[cache_key] if @seed_availability_cache.key?(cache_key) - return "You don't have any seeds of this crop." if seeds.none? + @seed_availability_cache[cache_key] = begin + seeds = member.seeds.where(crop:) - if total_quantity == 0 - "You have an unknown quantity of seeds of this crop." - else - "You have #{total_quantity} #{Seed.model_name.human(count: total_quantity)} of this crop." + if seeds.none? + "You don't have any seeds of this crop." + else + total_quantity = seeds.where.not(quantity: nil).sum(:quantity) + if total_quantity == 0 + "You have an unknown quantity of seeds of this crop." + else + "You have #{total_quantity} #{Seed.model_name.human(count: total_quantity)} of this crop." + end + end end end @@ -40,53 +59,57 @@ module CropsHelper end def crop_jsonld_data(crop, full_attributes: true) - same_as_urls = [crop.en_wikipedia_url] - crop.scientific_names.each do |scientific_name| - same_as_urls << "https://www.wikidata.org/wiki/#{scientific_name.wikidata_id}" if scientific_name.wikidata_id.present? - end - - subject_of_entities = [] - if full_attributes - if crop.en_youtube_url.present? - subject_of_entities << { - '@type': "VideoObject", - url: crop.en_youtube_url - } - end - - crop.posts.each do |post| - subject_of_entities << { - '@type': "SocialMediaPosting", - url: post_url(post), - author: { - '@type': 'Person', - name: post.author.login_name - }, - datePublished: post.created_at - } + Rails.cache.fetch([crop.cache_key_with_version, "jsonld", full_attributes]) do + same_as_urls = [crop.en_wikipedia_url] + crop.scientific_names.each do |scientific_name| + if scientific_name.wikidata_id.present? + same_as_urls << "https://www.wikidata.org/wiki/#{scientific_name.wikidata_id}" + end end + subject_of_entities = [] images = [] - crop.photos.each do |photo| - images << photo.fullsize_url + if full_attributes + if crop.en_youtube_url.present? + subject_of_entities << { + '@type': "VideoObject", + url: crop.en_youtube_url + } + end + + crop.posts.each do |post| + subject_of_entities << { + '@type': "SocialMediaPosting", + url: post_url(post), + author: { + '@type': 'Person', + name: post.author.login_name + }, + datePublished: post.created_at + } + end + + crop.photos.each do |photo| + images << photo.fullsize_url + end end + + # TODO: Review plantings, seeds, harvests as a subtype of social media post or event that ended? Or creative work? + # has_many :plantings, dependent: :destroy + # has_many :seeds, dependent: :destroy + # has_many :harvests, dependent: :destroy + + { + '@context': "https://schema.org", + '@type': "BioChemEntity", + name: crop.name, + taxonomicRange: crop.scientific_names.map(&:name), + description: crop.description, + sameAs: same_as_urls, + alternateName: crop.alternate_names.map(&:name), + subjectOf: subject_of_entities, + image: images + }.compact end - - # TODO: Review plantings, seeds, harvests as a subtype of social media post or event that ended? Or creative work? - # has_many :plantings, dependent: :destroy - # has_many :seeds, dependent: :destroy - # has_many :harvests, dependent: :destroy - - { - '@context': "https://schema.org", - '@type': "BioChemEntity", - name: crop.name, - taxonomicRange: crop.scientific_names.map(&:name), - description: crop.description, - sameAs: same_as_urls, - alternateName: crop.alternate_names.map(&:name), - subjectOf: subject_of_entities, - image: images - }.compact end end From 0df7589feb6a42c500e5490018ed5ff3b61b7062 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:59:06 +0000 Subject: [PATCH 18/42] Optimize Harvests with memoization and fragment caching - Memoize display methods in `Harvest` model. - Memoize calculation methods in `PredictHarvest` concern using `defined?` for nil safety. - Add fragment caching to `app/views/harvests/_popover.html.haml`. - Add fragment caching to `app/views/crops/_harvests.html.haml` with daily expiration for relative time strings. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> --- app/models/concerns/predict_harvest.rb | 20 +++++++---- app/models/harvest.rb | 50 ++++++++++++++++---------- app/views/crops/_harvests.html.haml | 15 ++++---- app/views/harvests/_popover.html.haml | 7 ++-- 4 files changed, 57 insertions(+), 35 deletions(-) diff --git a/app/models/concerns/predict_harvest.rb b/app/models/concerns/predict_harvest.rb index 64d7063d2..4a839fdef 100644 --- a/app/models/concerns/predict_harvest.rb +++ b/app/models/concerns/predict_harvest.rb @@ -6,23 +6,31 @@ module PredictHarvest included do # dates def first_harvest_date - harvests_with_dates.minimum(:harvested_at) + return @first_harvest_date if defined?(@first_harvest_date) + + @first_harvest_date = harvests_with_dates.minimum(:harvested_at) end def last_harvest_date - harvests_with_dates.maximum(:harvested_at) + return @last_harvest_date if defined?(@last_harvest_date) + + @last_harvest_date = harvests_with_dates.maximum(:harvested_at) end def first_harvest_predicted_at - return unless crop.median_days_to_first_harvest.present? && planted_at.present? + return @first_harvest_predicted_at if defined?(@first_harvest_predicted_at) - planted_at + crop.median_days_to_first_harvest.days + @first_harvest_predicted_at = if crop.median_days_to_first_harvest.present? && planted_at.present? + planted_at + crop.median_days_to_first_harvest.days + end end def last_harvest_predicted_at - return unless crop.median_days_to_last_harvest.present? && planted_at.present? + return @last_harvest_predicted_at if defined?(@last_harvest_predicted_at) - planted_at + crop.median_days_to_last_harvest.days + @last_harvest_predicted_at = if crop.median_days_to_last_harvest.present? && planted_at.present? + planted_at + crop.median_days_to_last_harvest.days + end end # actions diff --git a/app/models/harvest.rb b/app/models/harvest.rb index 92ed0f6eb..5b2b05cfe 100644 --- a/app/models/harvest.rb +++ b/app/models/harvest.rb @@ -109,37 +109,49 @@ class Harvest < ApplicationRecord def to_s # 50 individual apples, weighing 3lb # 2 buckets of apricots, weighing 10kg - "#{quantity_to_human} #{unit_to_human} #{crop_name_to_human} #{weight_to_human}".strip + @to_s ||= "#{quantity_to_human} #{unit_to_human} #{crop_name_to_human} #{weight_to_human}".strip end def quantity_to_human - return number_to_human(quantity.to_s, strip_insignificant_zeros: true) if quantity - - "" + @quantity_to_human ||= if quantity + number_to_human(quantity.to_s, strip_insignificant_zeros: true) + else + "" + end end def unit_to_human - return "" unless quantity && unit - return 'individual' if unit == 'individual' - return "#{unit} of" if quantity == 1 - - "#{unit.pluralize} of" + @unit_to_human ||= begin + if !quantity || !unit + "" + elsif unit == 'individual' + 'individual' + elsif quantity == 1 + "#{unit} of" + else + "#{unit.pluralize} of" + end + end end def weight_to_human - return "" unless weight_quantity - - "weighing #{number_to_human(weight_quantity, strip_insignificant_zeros: true)} #{weight_unit}" + @weight_to_human ||= if weight_quantity + "weighing #{number_to_human(weight_quantity, strip_insignificant_zeros: true)} #{weight_unit}" + else + "" + end end def crop_name_to_human - if unit != 'individual' # buckets of apricot*s* - crop.name.pluralize - elsif quantity == 1 - crop.name - else - crop.name.pluralize - end.to_s + @crop_name_to_human ||= begin + if unit != 'individual' # buckets of apricot*s* + crop.name.pluralize + elsif quantity == 1 + crop.name + else + crop.name.pluralize + end.to_s + end end private diff --git a/app/views/crops/_harvests.html.haml b/app/views/crops/_harvests.html.haml index 47f43d57a..aaa09319b 100644 --- a/app/views/crops/_harvests.html.haml +++ b/app/views/crops/_harvests.html.haml @@ -6,13 +6,14 @@ - unless crop.harvests.empty? %ul.list-group.list-group-flush - Harvest.where(crop: crop).includes(:owner).order(harvested_at: :desc).limit(3).each do |harvest| - %li.list-group-item - = link_to harvest_path(harvest), class: 'card-link' do - = harvest_icon - #{harvest.owner} harvested #{display_quantity(harvest)}. - .float-right= render 'members/location', member: harvest.owner - .harvest-timeago - %small #{standard_time_distance(harvest.harvested_at, Time.zone.now.to_date)} + - cache [harvest, Time.zone.today] do + %li.list-group-item + = link_to harvest_path(harvest), class: 'card-link' do + = harvest_icon + #{harvest.owner} harvested #{display_quantity(harvest)}. + .float-right= render 'members/location', member: harvest.owner + .harvest-timeago + %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/_popover.html.haml b/app/views/harvests/_popover.html.haml index 508413918..f16f141e7 100644 --- a/app/views/harvests/_popover.html.haml +++ b/app/views/harvests/_popover.html.haml @@ -1,3 +1,4 @@ -%p - %small - = harvest.harvested_at +- cache harvest do + %p + %small + = harvest.harvested_at From 3c70ba12ca22f9ccdf4219b679e3ea551c718eb5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:59:21 +0000 Subject: [PATCH 19/42] Allow filtering Flickr photos by tag when adding photos - Update MemberFlickr concern to support tag-based search using flickr.photos.search - Update PhotosController to handle the 'tag' parameter - Add tag search input field to the 'New Photo' view - Add test case to verify tag filtering in PhotosController spec Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> --- app/controllers/photos_controller.rb | 3 ++- app/models/concerns/member_flickr.rb | 11 +++++++++-- app/views/photos/new.html.haml | 15 ++++++++------- spec/controllers/photos_controller_spec.rb | 11 +++++++++++ 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index a10bb430d..94f1e8d16 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -102,11 +102,12 @@ class PhotosController < ApplicationController end @current_set = params[:set] + @current_tag = params[:tag] page = params[:page] || 1 @sets = current_member.flickr_sets - photos, total = current_member.flickr_photos(page, @current_set) + photos, total = current_member.flickr_photos(page, @current_set, @current_tag) @photos = WillPaginate::Collection.create(page, 30, total) do |pager| pager.replace photos diff --git a/app/models/concerns/member_flickr.rb b/app/models/concerns/member_flickr.rb index fb13a94e9..71291d6d2 100644 --- a/app/models/concerns/member_flickr.rb +++ b/app/models/concerns/member_flickr.rb @@ -40,8 +40,15 @@ module MemberFlickr # Fetches a collection of photos from Flickr # Returns a [[page of photos], total] pair. # Total is needed for pagination. - def flickr_photos(page_num = 1, set = nil) - result = if set + def flickr_photos(page_num = 1, set = nil, tags = nil) + result = if tags.present? + flickr.photos.search( + user_id: 'me', + tags: tags, + page: page_num, + per_page: 30 + ) + elsif set.present? flickr.photosets.getPhotos( photoset_id: set, page: page_num, diff --git a/app/views/photos/new.html.haml b/app/views/photos/new.html.haml index 9a444619b..cfdedecf3 100644 --- a/app/views/photos/new.html.haml +++ b/app/views/photos/new.html.haml @@ -21,13 +21,14 @@ Please select a photo from your recent uploads. - - if @sets && !@sets.empty? - %p - = bootstrap_form_tag(url: new_photo_path, method: :get, layout: :inline) do |f| - = f.select :set, options_for_select(@sets, @current_set), label: "Choose a photo album" - = hidden_field_tag :type, @type - = hidden_field_tag :id, @id - = f.submit "Search", class: "btn btn-success" + %p + = bootstrap_form_tag(url: new_photo_path, method: :get, layout: :inline) do |f| + - if @sets && !@sets.empty? + = f.select :set, options_for_select(@sets, @current_set), label: "Choose a photo album", include_blank: true + = f.text_field :tag, value: @current_tag, label: "or search by tag" + = hidden_field_tag :type, @type + = hidden_field_tag :id, @id + = f.submit "Search", class: "btn btn-success" - if @sets && @current_set %h2= @sets.key(@current_set) diff --git a/spec/controllers/photos_controller_spec.rb b/spec/controllers/photos_controller_spec.rb index 5347d8e1f..a35b0db68 100644 --- a/spec/controllers/photos_controller_spec.rb +++ b/spec/controllers/photos_controller_spec.rb @@ -60,6 +60,7 @@ describe PhotosController, :search do sign_in member member.stub(:flickr_photos) { [[], 0] } member.stub(:flickr_sets) { { "foo" => "bar" } } + member.stub(:flickr_auth_valid?) { true } controller.stub(:current_member) { member } end @@ -85,6 +86,16 @@ describe PhotosController, :search do it { expect(assigns(:item)).to eq garden } it { expect(flash[:alert]).not_to be_present } end + + describe "filtering by tag" do + let(:tag) { "tomato" } + + it "passes the tag to flickr_photos" do + expect(member).to receive(:flickr_photos).with(anything, nil, tag).and_return([[], 0]) + get :new, params: { type: "planting", id: planting.id, tag: tag } + expect(assigns(:current_tag)).to eq tag + end + end end describe "POST create" do From 2e0c8a910d1a495c8ba1d2c408f54bce312d7583 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:07:18 +0000 Subject: [PATCH 20/42] Memoize Planting-related methods for performance optimization This commit introduces memoization to various methods in the Planting model, PredictPlanting and PredictHarvest concerns, PlantingsHelper, and PlantingsController. Specifically: - Memoized database-intensive lookups like `nearby_same_crop`, `first_harvest_date`, and `last_harvest_date`. - Memoized calculated fields like `finish_predicted_at`, `expected_lifespan`, and `age_in_days`. - Optimized `PlantingsHelper#transplantable_gardens_by_owner` using a hash to cache results per planting instance within a request. - Applied the `defined?(@variable)` pattern where appropriate to ensure efficient handling of `nil` results. These changes reduce redundant database queries and expensive calculations, particularly during view rendering where these methods are frequently accessed. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> --- app/controllers/plantings_controller.rb | 2 +- app/helpers/plantings_helper.rb | 8 +++- app/models/concerns/predict_harvest.rb | 40 +++++++++++-------- app/models/concerns/predict_planting.rb | 51 +++++++++++++++---------- app/models/planting.rb | 18 +++++---- 5 files changed, 73 insertions(+), 46 deletions(-) diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index d903a25da..2d7a26d17 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -160,7 +160,7 @@ class PlantingsController < DataController end def matching_seeds - Seed.where(crop: @planting.crop, owner: @planting.owner) + @matching_seeds ||= Seed.where(crop: @planting.crop, owner: @planting.owner) .where('(finished_at IS NULL OR finished_at >= ?)', @planting.planted_at) .where('(saved_at IS NULL OR saved_at <= ?)', @planting.planted_at) end diff --git a/app/helpers/plantings_helper.rb b/app/helpers/plantings_helper.rb index 1d5345151..04d9203d1 100644 --- a/app/helpers/plantings_helper.rb +++ b/app/helpers/plantings_helper.rb @@ -46,9 +46,13 @@ module PlantingsHelper # Returns a list of gardens the planting can be transplanted to # based on the planting's owner. def transplantable_gardens_by_owner(planting) - garden_ids = planting.owner.gardens.select(:id).to_a + GardenCollaborator.where(member_id: planting.owner.id).select(:garden_id).to_a + @transplantable_gardens ||= {} + cache_key = planting.id || planting.object_id + @transplantable_gardens[cache_key] ||= begin + garden_ids = planting.owner.gardens.select(:id).to_a + GardenCollaborator.where(member_id: planting.owner.id).select(:garden_id).to_a - Garden.active.where.not(id: planting.garden_id).where(id: garden_ids) + Garden.active.where.not(id: planting.garden_id).where(id: garden_ids) + end end def days_from_now_to_last_harvest(planting) diff --git a/app/models/concerns/predict_harvest.rb b/app/models/concerns/predict_harvest.rb index 64d7063d2..d68345252 100644 --- a/app/models/concerns/predict_harvest.rb +++ b/app/models/concerns/predict_harvest.rb @@ -6,23 +6,31 @@ module PredictHarvest included do # dates def first_harvest_date - harvests_with_dates.minimum(:harvested_at) + return @first_harvest_date if defined?(@first_harvest_date) + + @first_harvest_date = harvests_with_dates.minimum(:harvested_at) end def last_harvest_date - harvests_with_dates.maximum(:harvested_at) + return @last_harvest_date if defined?(@last_harvest_date) + + @last_harvest_date = harvests_with_dates.maximum(:harvested_at) end def first_harvest_predicted_at - return unless crop.median_days_to_first_harvest.present? && planted_at.present? + return @first_harvest_predicted_at if defined?(@first_harvest_predicted_at) - planted_at + crop.median_days_to_first_harvest.days + @first_harvest_predicted_at = if crop.median_days_to_first_harvest.present? && planted_at.present? + planted_at + crop.median_days_to_first_harvest.days + end end def last_harvest_predicted_at - return unless crop.median_days_to_last_harvest.present? && planted_at.present? + return @last_harvest_predicted_at if defined?(@last_harvest_predicted_at) - planted_at + crop.median_days_to_last_harvest.days + @last_harvest_predicted_at = if crop.median_days_to_last_harvest.present? && planted_at.present? + planted_at + crop.median_days_to_last_harvest.days + end end # actions @@ -65,16 +73,18 @@ module PredictHarvest end def neighbours_for_harvest_predictions - # use this planting's harvest if any - return harvests if harvests.size.positive? - - # otherwise use nearby plantings - if location - return Harvest.where(planting: nearby_same_crop.has_harvests) - .where.not(planting_id: nil) + @neighbours_for_harvest_predictions ||= begin + # use this planting's harvest if any + if harvests.size.positive? + harvests + # otherwise use nearby plantings + elsif location + Harvest.where(planting: nearby_same_crop.has_harvests) + .where.not(planting_id: nil) + else + Harvest.none + end end - - Harvest.none end private diff --git a/app/models/concerns/predict_planting.rb b/app/models/concerns/predict_planting.rb index ab158cac0..5162513cb 100644 --- a/app/models/concerns/predict_planting.rb +++ b/app/models/concerns/predict_planting.rb @@ -13,40 +13,49 @@ module PredictPlanting # dates def finish_predicted_at - if planted_at.blank? || failed? - nil - elsif crop.median_lifespan.present? - planted_at + crop.median_lifespan.days - elsif crop.parent.present? && crop.parent.median_lifespan.present? - planted_at + crop.parent.median_lifespan.days - end + return @finish_predicted_at if defined?(@finish_predicted_at) + + @finish_predicted_at = if planted_at.blank? || failed? + nil + elsif crop.median_lifespan.present? + planted_at + crop.median_lifespan.days + elsif crop.parent.present? && crop.parent.median_lifespan.present? + planted_at + crop.parent.median_lifespan.days + end end # days def expected_lifespan - if actual_lifespan.present? - actual_lifespan - elsif crop.median_lifespan.present? - crop.median_lifespan - elsif crop.parent.present? && crop.parent.median_lifespan.present? - crop.parent.median_lifespan - end + return @expected_lifespan if defined?(@expected_lifespan) + + @expected_lifespan = if actual_lifespan.present? + actual_lifespan + elsif crop.median_lifespan.present? + crop.median_lifespan + elsif crop.parent.present? && crop.parent.median_lifespan.present? + crop.parent.median_lifespan + end end def actual_lifespan - return unless planted_at.present? && finished_at.present? && !failed? + return @actual_lifespan if defined?(@actual_lifespan) - (finished_at - planted_at).to_i + @actual_lifespan = if planted_at.present? && finished_at.present? && !failed? + (finished_at - planted_at).to_i + end end def age_in_days - return if planted_at.blank? - return if failed? + return @age_in_days if defined?(@age_in_days) - known_last_day ||= finished_at || Time.zone.today - known_last_day = Time.zone.today if known_last_day > Time.zone.today + @age_in_days = if planted_at.blank? || failed? + nil + else + known_last_day = finished_at || Time.zone.today + known_last_day = Time.zone.today if known_last_day > Time.zone.today - (known_last_day - planted_at).to_i + (known_last_day - planted_at).to_i + end end def percentage_grown diff --git a/app/models/planting.rb b/app/models/planting.rb index 05ecc99ec..6b90bc3b8 100644 --- a/app/models/planting.rb +++ b/app/models/planting.rb @@ -119,14 +119,18 @@ class Planting < ApplicationRecord end def nearby_same_crop - return Planting.none if location.blank? || latitude.blank? || longitude.blank? + return @nearby_same_crop if defined?(@nearby_same_crop) - # latitude, longitude = Geocoder.coordinates(location, params: { limit: 1 }) - Planting.joins(:garden) - .where(crop:) - .located - .where('gardens.latitude < ? AND gardens.latitude > ?', - latitude + 10, latitude - 10) + @nearby_same_crop = if location.blank? || latitude.blank? || longitude.blank? + Planting.none + else + # latitude, longitude = Geocoder.coordinates(location, params: { limit: 1 }) + Planting.joins(:garden) + .where(crop:) + .located + .where('gardens.latitude < ? AND gardens.latitude > ?', + latitude + 10, latitude - 10) + end end private From 50ab6f39eee4304a106cad126f6563dbebde930b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:16:36 +0000 Subject: [PATCH 21/42] Optimize Harvests with memoization and caching - Memoize display methods in `Harvest` model. - Memoize calculation methods in `PredictHarvest` concern using `defined?` for nil safety. - Add fragment caching to `app/views/harvests/_popover.html.haml`. - Add fragment caching and query caching to `app/views/crops/_harvests.html.haml` with daily expiration for relative time strings. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> --- app/views/crops/_harvests.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/crops/_harvests.html.haml b/app/views/crops/_harvests.html.haml index aaa09319b..1d80f8b27 100644 --- a/app/views/crops/_harvests.html.haml +++ b/app/views/crops/_harvests.html.haml @@ -5,7 +5,8 @@ %p Nobody has harvested this crop yet. - unless crop.harvests.empty? %ul.list-group.list-group-flush - - Harvest.where(crop: crop).includes(:owner).order(harvested_at: :desc).limit(3).each do |harvest| + - Rails.cache.fetch([crop, "recent_harvests", Time.zone.today]) do + - Harvest.where(crop: crop).includes(:owner).order(harvested_at: :desc).limit(3).each do |harvest| - cache [harvest, Time.zone.today] do %li.list-group-item = link_to harvest_path(harvest), class: 'card-link' do From 22638371c245141e89b5bf17c0f171ea883aece2 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 28 Apr 2026 13:01:02 +0930 Subject: [PATCH 22/42] Update _harvests.html.haml --- app/views/crops/_harvests.html.haml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/crops/_harvests.html.haml b/app/views/crops/_harvests.html.haml index 1d80f8b27..54ecf142a 100644 --- a/app/views/crops/_harvests.html.haml +++ b/app/views/crops/_harvests.html.haml @@ -7,7 +7,6 @@ %ul.list-group.list-group-flush - Rails.cache.fetch([crop, "recent_harvests", Time.zone.today]) do - Harvest.where(crop: crop).includes(:owner).order(harvested_at: :desc).limit(3).each do |harvest| - - cache [harvest, Time.zone.today] do %li.list-group-item = link_to harvest_path(harvest), class: 'card-link' do = harvest_icon From f24ca80394c88298ac957d6e789a66c781f99f00 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 28 Apr 2026 03:46:47 +0000 Subject: [PATCH 23/42] Fix various breadcrumb links to avoid passing ?owner, which doesn't actually filter --- app/views/activities/index.html.haml | 2 +- app/views/harvests/index.html.haml | 2 +- app/views/plantings/index.html.haml | 2 +- app/views/seeds/index.html.haml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/activities/index.html.haml b/app/views/activities/index.html.haml index 9a87761f3..9326d94bf 100644 --- a/app/views/activities/index.html.haml +++ b/app/views/activities/index.html.haml @@ -3,7 +3,7 @@ - content_for :breadcrumbs do - if @owner %li.breadcrumb-item= link_to 'Activities', activities_path - %li.breadcrumb-item.active= link_to "#{@owner}'s activities", activities_path(owner: @owner) + %li.breadcrumb-item.active= link_to "#{@owner}'s activities", activities_path(@owner) - else %li.breadcrumb-item.active= link_to 'Activities', activities_path diff --git a/app/views/harvests/index.html.haml b/app/views/harvests/index.html.haml index 7038e5249..16dc966d2 100644 --- a/app/views/harvests/index.html.haml +++ b/app/views/harvests/index.html.haml @@ -4,7 +4,7 @@ - content_for :breadcrumbs do - if @owner %li.breadcrumb-item= link_to 'Harvests', harvests_path - %li.breadcrumb-item.active= link_to "#{@owner}'s harvests", harvests_path(owner: @owner) + %li.breadcrumb-item.active= link_to "#{@owner}'s harvests", harvests_path(@owner) - else %li.breadcrumb-item.active= link_to "Harvests", harvests_path .row diff --git a/app/views/plantings/index.html.haml b/app/views/plantings/index.html.haml index 9d01c9b3e..17b7c627b 100644 --- a/app/views/plantings/index.html.haml +++ b/app/views/plantings/index.html.haml @@ -4,7 +4,7 @@ - content_for :breadcrumbs do - if @owner %li.breadcrumb-item= link_to 'Plantings', plantings_path - %li.breadcrumb-item.active= link_to "#{@owner}'s plantings", plantings_path(owner: @owner) + %li.breadcrumb-item.active= link_to "#{@owner}'s plantings", plantings_path(@owner) - else %li.breadcrumb-item.active= link_to 'Plantings', plantings_path diff --git a/app/views/seeds/index.html.haml b/app/views/seeds/index.html.haml index 619379779..85f1a15fe 100644 --- a/app/views/seeds/index.html.haml +++ b/app/views/seeds/index.html.haml @@ -1,7 +1,7 @@ - content_for :breadcrumbs do - if @owner %li.breadcrumb-item= link_to 'Seeds', seeds_path - %li.breadcrumb-item.active= link_to "#{@owner}'s seeds", seeds_path(owner: @owner) + %li.breadcrumb-item.active= link_to "#{@owner}'s seeds", seeds_path(@owner) - else %li.breadcrumb-item.active= link_to 'Seeds', seeds_path From 60390fcc068cbbb7f12995aa5386bcf0b5681e40 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 28 Apr 2026 04:03:27 +0000 Subject: [PATCH 24/42] Fix links further --- app/views/activities/index.html.haml | 2 +- app/views/harvests/index.html.haml | 2 +- app/views/plantings/index.html.haml | 2 +- app/views/seeds/index.html.haml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/activities/index.html.haml b/app/views/activities/index.html.haml index 9326d94bf..8addf9e35 100644 --- a/app/views/activities/index.html.haml +++ b/app/views/activities/index.html.haml @@ -3,7 +3,7 @@ - content_for :breadcrumbs do - if @owner %li.breadcrumb-item= link_to 'Activities', activities_path - %li.breadcrumb-item.active= link_to "#{@owner}'s activities", activities_path(@owner) + %li.breadcrumb-item.active= link_to "#{@owner}'s activities", member_activities_path(@owner) - else %li.breadcrumb-item.active= link_to 'Activities', activities_path diff --git a/app/views/harvests/index.html.haml b/app/views/harvests/index.html.haml index 16dc966d2..7c281e08b 100644 --- a/app/views/harvests/index.html.haml +++ b/app/views/harvests/index.html.haml @@ -4,7 +4,7 @@ - content_for :breadcrumbs do - if @owner %li.breadcrumb-item= link_to 'Harvests', harvests_path - %li.breadcrumb-item.active= link_to "#{@owner}'s harvests", harvests_path(@owner) + %li.breadcrumb-item.active= link_to "#{@owner}'s harvests", member_harvests_path(@owner) - else %li.breadcrumb-item.active= link_to "Harvests", harvests_path .row diff --git a/app/views/plantings/index.html.haml b/app/views/plantings/index.html.haml index 17b7c627b..025d0ac09 100644 --- a/app/views/plantings/index.html.haml +++ b/app/views/plantings/index.html.haml @@ -4,7 +4,7 @@ - content_for :breadcrumbs do - if @owner %li.breadcrumb-item= link_to 'Plantings', plantings_path - %li.breadcrumb-item.active= link_to "#{@owner}'s plantings", plantings_path(@owner) + %li.breadcrumb-item.active= link_to "#{@owner}'s plantings", member_plantings_path(@owner) - else %li.breadcrumb-item.active= link_to 'Plantings', plantings_path diff --git a/app/views/seeds/index.html.haml b/app/views/seeds/index.html.haml index 85f1a15fe..b898fb735 100644 --- a/app/views/seeds/index.html.haml +++ b/app/views/seeds/index.html.haml @@ -1,7 +1,7 @@ - content_for :breadcrumbs do - if @owner %li.breadcrumb-item= link_to 'Seeds', seeds_path - %li.breadcrumb-item.active= link_to "#{@owner}'s seeds", seeds_path(@owner) + %li.breadcrumb-item.active= link_to "#{@owner}'s seeds", member_seeds_path(@owner) - else %li.breadcrumb-item.active= link_to 'Seeds', seeds_path From 7160f50ac15337f563388d8923b3c6265ff8af47 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 28 Apr 2026 17:51:01 +0930 Subject: [PATCH 25/42] Refactor Activity model to remove Elasticsearch integration (#4576) * Refactor Activity model to remove Elasticsearch integration - Removed `SearchActivities` concern and Searchkick from `Activity` model. - Implemented `Activity.homepage_records` using ActiveRecord with `DISTINCT ON` for PostgreSQL. - Updated `ActivitiesController#index` to use ActiveRecord queries with eager loading and pagination. - Added `active` scope to `Activity`. - Added unit tests for `Activity` model. - Deleted `app/models/concerns/search_activities.rb`. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> * Fix NoMethodError: undefined method 'reindex' for class Activity - Removed all calls to `Activity.reindex` in migrations, rake tasks, and spec helpers. - These were causing failures after the removal of Searchkick from the Activity model. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> * Fix ambiguous column id in homepage_records query - Updated `Activity.homepage_records` to use `activities.id` instead of `id` in the subquery. - This resolves the `PG::AmbiguousColumn: ERROR: column reference "id" is ambiguous` error. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> * Fix ambiguous created_at in homepage_records query - Use `unscoped` in the subquery for `Activity.homepage_records` to bypass the default scope from `Ownable` concern. - This prevents the join with the `members` table in the subquery, which was causing `PG::AmbiguousColumn: ERROR: column reference "created_at" is ambiguous`. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> * Complete refactoring of Activity model to remove Elasticsearch - Removed SearchActivities concern and searchkick integration. - Updated ActivitiesController#index to use ActiveRecord queries. - Implemented performant Activity.homepage_records using DISTINCT ON (PostgreSQL). - Added Activity.active scope. - Added no-op Activity.reindex (class and instance methods) for backward compatibility. - Cleaned up leftover reindex calls in rake tasks, migrations, and spec helpers. - Added unit tests for new Activity model logic. - Updated factories to include no-op reindex traits. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> * Less eager loading --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- app/controllers/activities_controller.rb | 15 ++--- app/models/activity.rb | 14 ++++- app/models/concerns/search_activities.rb | 66 --------------------- db/migrate/20240218035144_add_activities.rb | 2 - lib/tasks/search.rake | 1 - spec/factories/activity.rb | 4 ++ spec/factories/post.rb | 4 ++ spec/models/activity_spec.rb | 42 +++++++++++++ spec/spec_helper.rb | 1 - 9 files changed, 68 insertions(+), 81 deletions(-) delete mode 100644 app/models/concerns/search_activities.rb create mode 100644 spec/models/activity_spec.rb diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb index 86b22bf4f..ab3948ac9 100644 --- a/app/controllers/activities_controller.rb +++ b/app/controllers/activities_controller.rb @@ -4,21 +4,16 @@ class ActivitiesController < DataController def index @show_all = params[:all] == '1' - where = {} - where['active'] = true unless @show_all + @activities = Activity.includes(:owner).order(created_at: :desc) + @activities = @activities.active unless @show_all if params[:member_slug].present? @owner = Member.find_by!(slug: params[:member_slug]) - where['owner_id'] = @owner.id + @activities = @activities.where(owner_id: @owner.id) end - @activities = Activity.search( - where:, - page: params[:page], - limit: 30, - boost_by: [:created_at], - load: false - ) + @activities = @activities.paginate(page: params[:page], per_page: 30) + @filename = "Growstuff-#{specifics}Activities-#{Time.zone.now.to_fs(:number)}.csv" respond_with(@activities) end diff --git a/app/models/activity.rb b/app/models/activity.rb index 30bde60e1..f9e6b63a5 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -4,7 +4,6 @@ class Activity < ApplicationRecord extend FriendlyId include Ownable include Finishable - include SearchActivities include Likeable belongs_to :garden, optional: true @@ -46,4 +45,17 @@ class Activity < ApplicationRecord def planting_slug planting&.crop&.slug end + + scope :active, -> { where(finished: [false, nil]) } + + def self.homepage_records(limit) + # Get the latest activity for each owner, then return the latest 'limit' of those + Activity.where(id: Activity.unscoped.select("DISTINCT ON (owner_id) id").order("owner_id, created_at DESC")) + .order(created_at: :desc) + .limit(limit) + end + + def self.reindex(refresh: false); end + + def reindex(refresh: false); end end diff --git a/app/models/concerns/search_activities.rb b/app/models/concerns/search_activities.rb deleted file mode 100644 index 8190b4a7c..000000000 --- a/app/models/concerns/search_activities.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -module SearchActivities - extend ActiveSupport::Concern - - included do - searchkick merge_mappings: true, - settings: { number_of_shards: 1, number_of_replicas: 0 }, - mappings: { - properties: { - active: { type: :boolean }, - created_at: { type: :integer }, - updated_at: { type: :integer }, - due_date: { type: :date } - } - } - - def search_data - { - slug:, - active:, - finished: finished?, - name:, - due_date:, - 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 - owner_id:, - owner_login_name:, - owner_slug:, - - # timestamps - created_at: created_at.to_i, - updated_at: updated_at.to_i - } - end - - def self.homepage_records(limit) - records = [] - owners = [] - 1..limit.times do - where = { - # photos_count: { gt: 0 }, - owner_id: { not: owners } - } - one_record = search('*', - limit: 1, - where:, - boost_by: [:created_at], - load: false).first - return records if one_record.nil? - - owners << one_record.owner_id - records << one_record - end - records - end - end -end diff --git a/db/migrate/20240218035144_add_activities.rb b/db/migrate/20240218035144_add_activities.rb index 956f5778f..8736947aa 100644 --- a/db/migrate/20240218035144_add_activities.rb +++ b/db/migrate/20240218035144_add_activities.rb @@ -15,7 +15,5 @@ class AddActivities < ActiveRecord::Migration[7.1] end add_column :members, :activities_count, :integer - - Activity.reindex end end diff --git a/lib/tasks/search.rake b/lib/tasks/search.rake index 8ac9180c4..396a9f33b 100644 --- a/lib/tasks/search.rake +++ b/lib/tasks/search.rake @@ -7,6 +7,5 @@ namespace :search do Planting.reindex Harvest.reindex Seed.reindex - Activity.reindex end end diff --git a/spec/factories/activity.rb b/spec/factories/activity.rb index 322de2ea0..91ab1480f 100644 --- a/spec/factories/activity.rb +++ b/spec/factories/activity.rb @@ -21,5 +21,9 @@ FactoryBot.define do description { "Stake tomato" } planting end + + trait :reindex do + # Activity is not using elasticsearch anymore, so we don't need to reindex + end end end diff --git a/spec/factories/post.rb b/spec/factories/post.rb index c035fdbe6..9079021c3 100644 --- a/spec/factories/post.rb +++ b/spec/factories/post.rb @@ -21,5 +21,9 @@ FactoryBot.define do factory :forum_post do forum end + + trait :reindex do + # Post is not using elasticsearch, but this trait is used in some tests + end end end diff --git a/spec/models/activity_spec.rb b/spec/models/activity_spec.rb new file mode 100644 index 000000000..56b74cdd6 --- /dev/null +++ b/spec/models/activity_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Activity do + describe '.homepage_records' do + let(:member1) { create(:member) } + let(:member2) { create(:member) } + + it 'returns the latest activities per owner' do + create(:activity, owner: member1, created_at: 2.days.ago) + latest_activity1 = create(:activity, owner: member1, created_at: 1.day.ago) + latest_activity2 = create(:activity, owner: member2, created_at: Time.current) + + records = described_class.homepage_records(10) + expect(records).to contain_exactly(latest_activity1, latest_activity2) + end + + it 'respects the limit' do + create(:activity, owner: member1) + create(:activity, owner: member2) + + records = described_class.homepage_records(1) + expect(records.length).to eq(1) + end + end + + describe 'active scope' do + it 'returns activities that are not finished' do + active_activity = create(:activity, finished: false) + finished_activity = create(:activity, finished: true) + + expect(described_class.active).to include(active_activity) + expect(described_class.active).not_to include(finished_activity) + end + + it 'treats nil finished as active' do + activity = create(:activity, finished: nil) + expect(described_class.active).to include(activity) + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0bd6f9a1c..d93ca5a64 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -50,7 +50,6 @@ RSpec.configure do |config| Photo.reindex Planting.reindex Seed.reindex - Activity.reindex end config.before(:suite) do From 64af597deca6ee467120604a4b5d2c7d009a0048 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 28 Apr 2026 18:10:50 +0930 Subject: [PATCH 26/42] Add funding information --- FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 FUNDING.yml diff --git a/FUNDING.yml b/FUNDING.yml new file mode 100644 index 000000000..1c39a2ab6 --- /dev/null +++ b/FUNDING.yml @@ -0,0 +1 @@ +ko_fi: jennyscottthompson From 6ce347af8284d2999581d037f210bfe19b00e8a8 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Tue, 28 Apr 2026 18:11:48 +0930 Subject: [PATCH 27/42] Rename FUNDING.yml to .github/FUNDING.yml --- FUNDING.yml => .github/FUNDING.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename FUNDING.yml => .github/FUNDING.yml (100%) diff --git a/FUNDING.yml b/.github/FUNDING.yml similarity index 100% rename from FUNDING.yml rename to .github/FUNDING.yml From e63089e03ba32afb72e5626549d176f10787d26d Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Thu, 30 Apr 2026 12:54:17 +0930 Subject: [PATCH 28/42] Remove deprecated config.read_encrypted_secrets from production.rb (#4603) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- config/environments/production.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config/environments/production.rb b/config/environments/production.rb index 4cf5e164a..81ce8904f 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -16,10 +16,6 @@ Rails.application.configure do config.consider_all_requests_local = false config.action_controller.perform_caching = true - # Attempt to read encrypted secrets from `config/secrets.yml.enc`. - # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or - # `config/secrets.yml.key`. - config.read_encrypted_secrets = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. From e423e6ac79da1860017b9129ac79a47878bab33f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 04:09:26 +0000 Subject: [PATCH 29/42] Add weekly harvest reminder emails and scheduled task - Added `send_harvest_reminder` preference to Member model and settings UI. - Implemented `harvest_in_next_week?` in PredictHarvest concern. - Created `harvest_reminder` email with localized templates. - Added `growstuff:send_harvest_reminders` Rake task to run weekly. - Refactored existing and new reminder tasks to use `deliver_later` for scalability. - Added unit tests for prediction logic and mailer. - Fixed a bug in the existing planting reminder task where it was using an uninitialized constant `Notifier`. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> --- app/controllers/application_controller.rb | 4 +- app/controllers/members_controller.rb | 5 ++- app/mailers/notifier_mailer.rb | 13 ++++++ app/models/concerns/predict_harvest.rb | 8 +++- app/models/member.rb | 1 + .../registrations/_edit_email.html.haml | 6 +++ .../harvest_reminder.html.haml | 33 +++++++++++++++ config/locales/en.yml | 6 +++ ...11_add_send_harvest_reminder_to_members.rb | 5 +++ db/schema.rb | 3 +- lib/tasks/growstuff.rake | 19 ++++++++- spec/mailers/harvest_reminder_mailer_spec.rb | 28 +++++++++++++ spec/models/harvest_prediction_spec.rb | 41 +++++++++++++++++++ 13 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 app/views/notifier_mailer/harvest_reminder.html.haml create mode 100644 db/migrate/20260429132911_add_send_harvest_reminder_to_members.rb create mode 100644 spec/mailers/harvest_reminder_mailer_spec.rb create mode 100644 spec/models/harvest_prediction_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7c7e58d6f..57846e7ed 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -68,7 +68,7 @@ class ApplicationController < ActionController::Base # profile stuff :bio, :location, :latitude, :longitude, # email settings - :show_email, :newsletter, :send_notification_email, :send_planting_reminder) + :show_email, :newsletter, :send_notification_email, :send_planting_reminder, :send_harvest_reminder) end devise_parameter_sanitizer.permit(:account_update) do |member| @@ -80,7 +80,7 @@ class ApplicationController < ActionController::Base :bio, :location, :latitude, :longitude, :website_url, :instagram_handle, :facebook_handle, :bluesky_handle, :other_url, # email settings - :show_email, :newsletter, :send_notification_email, :send_planting_reminder, + :show_email, :newsletter, :send_notification_email, :send_planting_reminder, :send_harvest_reminder, # update password :current_password) end diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index 160746519..6f6d8aa8b 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -90,11 +90,12 @@ class MembersController < ApplicationController EMAIL_TYPE_STRING = { send_notification_email: "direct message notifications", - send_planting_reminder: "planting reminders" + send_planting_reminder: "planting reminders", + send_harvest_reminder: "harvest reminders" }.freeze def member_params - params.require(:member).permit(:login_name, :tos_agreement, :email, :newsletter) + params.require(:member).permit(:login_name, :tos_agreement, :email, :newsletter, :send_harvest_reminder) end def member_json_fields diff --git a/app/mailers/notifier_mailer.rb b/app/mailers/notifier_mailer.rb index 4a85ea0f4..f4794656d 100644 --- a/app/mailers/notifier_mailer.rb +++ b/app/mailers/notifier_mailer.rb @@ -57,6 +57,19 @@ class NotifierMailer < ApplicationMailer mail(to: @member.email, subject: @subject) if @member.send_planting_reminder end + def harvest_reminder(member) + @member = member + @plantings = @member.plantings.active.select(&:harvest_in_next_week?) + @sitename = ENV.fetch('GROWSTUFF_SITE_NAME', nil) + @subject = I18n.t('notifier_mailer.harvest_reminder.subject', sitename: @sitename) + + # Encrypting + message = { member_id: @member.id, type: :send_harvest_reminder } + @signed_message = verifier.generate(message) + + mail(to: @member.email, subject: @subject) if @member.send_harvest_reminder + end + def new_crop_request(member, request) @member = member @request = request diff --git a/app/models/concerns/predict_harvest.rb b/app/models/concerns/predict_harvest.rb index d68345252..7ce6cc2cf 100644 --- a/app/models/concerns/predict_harvest.rb +++ b/app/models/concerns/predict_harvest.rb @@ -60,10 +60,16 @@ module PredictHarvest def before_harvest_time? first_harvest_predicted_at.present? && harvests.empty? && - first_harvest_predicted_at.present? && first_harvest_predicted_at > Time.zone.today end + def harvest_in_next_week? + first_harvest_predicted_at.present? && + harvests.empty? && + first_harvest_predicted_at >= Time.zone.today && + first_harvest_predicted_at <= Time.zone.today + 7.days + end + def harvest_months Rails.cache.fetch("#{cache_key_with_version}/harvest_months", expires_in: 5.minutes) do neighbours_for_harvest_predictions.where.not(harvested_at: nil) diff --git a/app/models/member.rb b/app/models/member.rb index f7eb7b326..82e5a6e41 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -79,6 +79,7 @@ class Member < ApplicationRecord scope :interesting, -> { confirmed.located.recently_signed_in.has_plantings } scope :has_plantings, -> { joins(:plantings).group("members.id") } scope :wants_reminders, -> { where(send_planting_reminder: true) } + scope :wants_harvest_reminders, -> { where(send_harvest_reminder: true) } # Include default devise modules. Others available are: # :token_authenticatable, :confirmable, diff --git a/app/views/devise/registrations/_edit_email.html.haml b/app/views/devise/registrations/_edit_email.html.haml index 6c9671215..1fc036231 100644 --- a/app/views/devise/registrations/_edit_email.html.haml +++ b/app/views/devise/registrations/_edit_email.html.haml @@ -28,6 +28,12 @@ = f.check_box :send_planting_reminder Receive regular reminders to track your planting and harvesting. + .form-group + .col-md-offset-2.col-md-8.checkbox + %label + = f.check_box :send_harvest_reminder + Receive regular reminders of upcoming harvests. + .form-group .col-md-offset-2.col-md-8.checkbox %label diff --git a/app/views/notifier_mailer/harvest_reminder.html.haml b/app/views/notifier_mailer/harvest_reminder.html.haml new file mode 100644 index 000000000..e12d11628 --- /dev/null +++ b/app/views/notifier_mailer/harvest_reminder.html.haml @@ -0,0 +1,33 @@ +%p Hello #{@member.login_name}, + +%h2= t('notifier_mailer.harvest_reminder.heading') + +%p= t('notifier_mailer.harvest_reminder.intro') + +%ul + - @plantings.each do |planting| + %li + = render 'planting', planting: planting + (Predicted harvest date: #{planting.first_harvest_predicted_at.to_date}) + +%p + Harvested anything lately? + = link_to "Track your harvests here.", new_harvest_url, class: 'btn' + +%p + Track and predict your entire garden, and keep your garden records up to date at + = link_to member_gardens_url(@member), class: 'btn' do + your garden overview + and on + = link_to member_url(@member) do + your profile page + +%h4 + See you soon on #{@sitename}! + += render partial: 'signature' + +%hr/ +%p + Don't want to get these emails any more? + = link_to t('notifier_mailer.harvest_reminder.unsubscribe'), unsubscribe_member_url(message: @signed_message) diff --git a/config/locales/en.yml b/config/locales/en.yml index 8885271fc..285bc0294 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -456,3 +456,9 @@ en: all: Not authorized to %{action} %{subject}. read: notification: You must be signed in to view notifications. + notifier_mailer: + harvest_reminder: + subject: "Upcoming harvests in your garden - %{sitename}" + heading: "Upcoming harvests in your garden" + intro: "Based on our predictions, the following plantings in your garden are likely to be ready for harvest within the next week:" + unsubscribe: "Unsubscribe from harvest reminders" diff --git a/db/migrate/20260429132911_add_send_harvest_reminder_to_members.rb b/db/migrate/20260429132911_add_send_harvest_reminder_to_members.rb new file mode 100644 index 000000000..068396bdd --- /dev/null +++ b/db/migrate/20260429132911_add_send_harvest_reminder_to_members.rb @@ -0,0 +1,5 @@ +class AddSendHarvestReminderToMembers < ActiveRecord::Migration[7.2] + def change + add_column :members, :send_harvest_reminder, :boolean, default: true, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 6cfdc7454..41c702c77 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_12_01_045000) do +ActiveRecord::Schema[7.2].define(version: 2026_04_29_132911) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -786,6 +786,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_12_01_045000) do t.string "facebook_handle" t.string "bluesky_handle" t.string "other_url" + t.boolean "send_harvest_reminder", default: true, null: false t.index ["confirmation_token"], name: "index_members_on_confirmation_token", unique: true t.index ["discarded_at"], name: "index_members_on_discarded_at" t.index ["email"], name: "index_members_on_email", unique: true diff --git a/lib/tasks/growstuff.rake b/lib/tasks/growstuff.rake index 4d0662b03..c73d7e087 100644 --- a/lib/tasks/growstuff.rake +++ b/lib/tasks/growstuff.rake @@ -48,8 +48,23 @@ namespace :growstuff do # Heroku scheduler only lets us run things daily, so this checks # Send on Monday if Time.zone.today.wday == 1 - Member.confirmed.wants_reminders.each do |m| - Notifier.planting_reminder(m).deliver_now! unless m.plantings.active.empty? + Member.confirmed.wants_reminders.find_each do |m| + NotifierMailer.planting_reminder(m).deliver_later unless m.plantings.active.empty? + end + end + end + + desc "Send harvest reminder email" + # usage: rake growstuff:send_harvest_reminders + + task send_harvest_reminders: :environment do + # Heroku scheduler only lets us run things daily, so this checks + # Send on Wednesday + if Time.zone.today.wday == 3 + Member.confirmed.wants_harvest_reminders.find_each do |m| + if m.plantings.active.any?(&:harvest_in_next_week?) + NotifierMailer.harvest_reminder(m).deliver_later + end end end end diff --git a/spec/mailers/harvest_reminder_mailer_spec.rb b/spec/mailers/harvest_reminder_mailer_spec.rb new file mode 100644 index 000000000..864c23b7f --- /dev/null +++ b/spec/mailers/harvest_reminder_mailer_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe NotifierMailer, type: :mailer do + let(:member) { create(:member) } + let(:mail) { NotifierMailer.harvest_reminder(member) } + + it "has a greeting" do + expect(mail.body.encoded).to match "Hello" + end + + context "when member has upcoming harvests" do + let(:crop) { create(:crop, median_days_to_first_harvest: 20) } + let!(:planting) { create(:planting, owner: member, crop: crop, planted_at: 15.days.ago) } + let(:plantings) { [planting] } + + it "lists the upcoming harvest" do + expect(mail.body.encoded).to match "Upcoming harvests in your garden" + expect(mail.body.encoded).to match planting.crop.name + expect(mail.body.encoded).to match (Time.zone.today + 5.days).to_date.to_s + end + + it "has an unsubscribe link" do + expect(mail.body.encoded).to match "Unsubscribe from harvest reminders" + end + end +end diff --git a/spec/models/harvest_prediction_spec.rb b/spec/models/harvest_prediction_spec.rb new file mode 100644 index 000000000..9501ead7a --- /dev/null +++ b/spec/models/harvest_prediction_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe "Harvest prediction logic" do + let(:crop) { create(:crop, median_days_to_first_harvest: 20) } + let(:planting) { create(:planting, crop: crop) } + + describe "#harvest_in_next_week?" do + it "is true if predicted harvest is in 5 days" do + planting.planted_at = 15.days.ago + expect(planting.harvest_in_next_week?).to be true + end + + it "is true if predicted harvest is today" do + planting.planted_at = 20.days.ago + expect(planting.harvest_in_next_week?).to be true + end + + it "is true if predicted harvest is in 7 days" do + planting.planted_at = 13.days.ago + expect(planting.harvest_in_next_week?).to be true + end + + it "is false if predicted harvest is in 8 days" do + planting.planted_at = 12.days.ago + expect(planting.harvest_in_next_week?).to be false + end + + it "is false if predicted harvest was yesterday" do + planting.planted_at = 21.days.ago + expect(planting.harvest_in_next_week?).to be false + end + + it "is false if there are already harvests" do + planting.planted_at = 15.days.ago + create(:harvest, planting: planting, owner: planting.owner, crop: planting.crop, harvested_at: 1.day.ago) + expect(planting.harvest_in_next_week?).to be false + end + end +end From 503ba716bbe7360cb18de0a572647c1fa3294c3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:04:07 +0000 Subject: [PATCH 30/42] Bump rubocop-capybara from 2.22.1 to 2.23.0 Bumps [rubocop-capybara](https://github.com/rubocop/rubocop-capybara) from 2.22.1 to 2.23.0. - [Release notes](https://github.com/rubocop/rubocop-capybara/releases) - [Changelog](https://github.com/rubocop/rubocop-capybara/blob/main/CHANGELOG.md) - [Commits](https://github.com/rubocop/rubocop-capybara/compare/v2.22.1...v2.23.0) --- updated-dependencies: - dependency-name: rubocop-capybara dependency-version: 2.23.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1bc3d997d..5955dac05 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -376,7 +376,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.19.3) + json (2.19.4) json-schema (6.2.0) addressable (~> 2.8) bigdecimal (>= 3.1, < 5) @@ -477,7 +477,7 @@ GEM paper_trail (17.0.0) activerecord (>= 7.1) request_store (~> 1.4) - parallel (2.0.1) + parallel (2.1.0) parser (3.3.11.1) ast (~> 2.4.1) racc @@ -644,9 +644,9 @@ GEM rubocop-ast (1.49.1) parser (>= 3.3.7.2) prism (~> 1.7) - rubocop-capybara (2.22.1) + rubocop-capybara (2.23.0) lint_roller (~> 1.1) - rubocop (~> 1.72, >= 1.72.1) + rubocop (~> 1.81) rubocop-factory_bot (2.28.0) lint_roller (~> 1.1) rubocop (~> 1.72, >= 1.72.1) From 4d1e8aede60c1fbdfd9b66aa4ea5d247740cb42b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 07:49:38 +0000 Subject: [PATCH 31/42] Bump axe-core-rspec from 4.11.2 to 4.11.3 Bumps [axe-core-rspec](https://github.com/dequelabs/axe-core-gems) from 4.11.2 to 4.11.3. - [Release notes](https://github.com/dequelabs/axe-core-gems/releases) - [Changelog](https://github.com/dequelabs/axe-core-gems/blob/develop/CHANGELOG.md) - [Commits](https://github.com/dequelabs/axe-core-gems/commits) --- updated-dependencies: - dependency-name: axe-core-rspec dependency-version: 4.11.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5955dac05..268da4a35 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -140,15 +140,15 @@ GEM aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) - axe-core-api (4.11.2) + axe-core-api (4.11.3) dumb_delegator ostruct virtus - axe-core-capybara (4.11.2) - axe-core-api (= 4.11.2) + axe-core-capybara (4.11.3) + axe-core-api (= 4.11.3) dumb_delegator - axe-core-rspec (4.11.2) - axe-core-api (= 4.11.2) + axe-core-rspec (4.11.3) + axe-core-api (= 4.11.3) dumb_delegator ostruct virtus From 5ac709ffd14df6d5e7523e737f9611d612e8dd08 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 11:30:22 +0000 Subject: [PATCH 32/42] Fix crash during CSV export of harvests and seeds When using Searchkick with `load: false`, search results are returned as HashResponse objects which do not support model associations or standard Rails URL helpers that expect model instances. This commit updates HarvestsController and SeedsController to conditionally load ActiveRecord objects when CSV format is requested, ensuring that the export templates can access the necessary associations. Similar logic was also applied to CropsController. Additionally, a typo in the Crops CSV shaper was fixed. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> --- app/controllers/crops_controller.rb | 2 +- app/controllers/harvests_controller.rb | 2 +- app/controllers/seeds_controller.rb | 2 +- app/views/crops/index.csv.shaper | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/crops_controller.rb b/app/controllers/crops_controller.rb index f9a0e2079..54ebcf56b 100644 --- a/app/controllers/crops_controller.rb +++ b/app/controllers/crops_controller.rb @@ -13,7 +13,7 @@ class CropsController < ApplicationController @crops = Crop.search('*', boost_by: %i(plantings_count harvests_count), limit: 100, page: params[:page], - load: false) + load: (request.format.csv? ? { include: %i(scientific_names parent creator) } : false)) @num_requested_crops = requested_crops.size if current_member @filename = filename respond_with @crops diff --git a/app/controllers/harvests_controller.rb b/app/controllers/harvests_controller.rb index 3b003c716..0efff94da 100644 --- a/app/controllers/harvests_controller.rb +++ b/app/controllers/harvests_controller.rb @@ -23,7 +23,7 @@ class HarvestsController < DataController @harvests = Harvest.search('*', where:, limit: 100, page: params[:page], - load: false, + load: (request.format.csv? ? { include: %i(crop owner plant_part) } : false), boost_by: [:created_at]) @filename = csv_filename diff --git a/app/controllers/seeds_controller.rb b/app/controllers/seeds_controller.rb index 897f34710..3b65a13d1 100644 --- a/app/controllers/seeds_controller.rb +++ b/app/controllers/seeds_controller.rb @@ -30,7 +30,7 @@ class SeedsController < DataController page: params[:page], limit: 30, boost_by: [:created_at], - load: false + load: (request.format.csv? ? { include: %i(crop owner) } : false) ) respond_with(@seeds) diff --git a/app/views/crops/index.csv.shaper b/app/views/crops/index.csv.shaper index c11b992a3..36509bcaf 100644 --- a/app/views/crops/index.csv.shaper +++ b/app/views/crops/index.csv.shaper @@ -58,7 +58,7 @@ csv.headers *all_headers end csv.cell :plantings_count, c.plantings_count || 0 - csv.cell :seeds_count, c.seeds.size[] + csv.cell :seeds_count, c.seeds.size csv.cell :harvests_count, c.harvests.size # Sunniness From 4643fbd92ec447cbd2fa981120b353ed22f2cdc7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 11:35:58 +0000 Subject: [PATCH 33/42] Associate post with crop from crop show page Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> --- app/controllers/posts_controller.rb | 4 ++++ app/views/crops/_actions.html.haml | 3 +++ app/views/crops/_posts.html.haml | 2 +- spec/controllers/posts_controller_spec.rb | 14 ++++++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 4b24953fb..90a400bb1 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -21,6 +21,10 @@ class PostsController < ApplicationController def new @post = Post.new @forum = Forum.find_by(id: params[:forum_id]) + if params[:crop_id] + @crop = Crop.friendly.find(params[:crop_id]) + @post.body = "[#{@crop.name}](crop)" + end respond_with(@post) end diff --git a/app/views/crops/_actions.html.haml b/app/views/crops/_actions.html.haml index 9f0d2d3a2..8aa1f7136 100644 --- a/app/views/crops/_actions.html.haml +++ b/app/views/crops/_actions.html.haml @@ -5,5 +5,8 @@ = render 'plantings/modal', planting: Planting.new(crop: crop, owner: current_member) = render 'harvests/modal', harvest: Harvest.new(crop: @crop, owner: current_member) = render 'seeds/modal', seed: Seed.new(crop: @crop, owner: current_member) + = link_to new_post_path(crop_id: crop.slug), class: 'btn', id: 'post-button' do + = post_icon + Post - if active_plantings.any? = render 'plantings/failed_modal', crop: crop, active_plantings: active_plantings diff --git a/app/views/crops/_posts.html.haml b/app/views/crops/_posts.html.haml index ad0c615ed..975087c66 100644 --- a/app/views/crops/_posts.html.haml +++ b/app/views/crops/_posts.html.haml @@ -6,7 +6,7 @@ Nobody has posted about #{crop.name.pluralize} yet. %p - if can? :create, Post - = link_to "Post something", new_post_path, class: 'btn btn-default' + = link_to "Post something", new_post_path(crop_id: crop.slug), class: 'btn btn-default' - else = render partial: "shared/signin_signup", locals: { to: "post your tips and experiences growing #{crop.name.pluralize}" } diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index 2e0cf3bf3..0462a930a 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -10,6 +10,20 @@ describe PostsController do { author_id: member.id, subject: "blah", body: "blah blah" } end + describe '#new' do + let(:crop) { create(:crop, name: 'Bush Bean') } + + it 'pre-populates the body when crop_id is present' do + get :new, params: { crop_id: crop.slug } + expect(assigns(:post).body).to eq("[#{crop.name}](crop)") + end + + it 'does not pre-populate the body when crop_id is absent' do + get :new + expect(assigns(:post).body).to be_nil + end + end + describe '#index' do before do create_list(:post, 100) From 914cfe99c84ee8c5443c20b3aecf433a0cc1b5d6 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sat, 2 May 2026 13:39:34 +0930 Subject: [PATCH 34/42] Fix seeds_count to correctly reference size --- app/views/crops/index.csv.shaper | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/crops/index.csv.shaper b/app/views/crops/index.csv.shaper index c11b992a3..36509bcaf 100644 --- a/app/views/crops/index.csv.shaper +++ b/app/views/crops/index.csv.shaper @@ -58,7 +58,7 @@ csv.headers *all_headers end csv.cell :plantings_count, c.plantings_count || 0 - csv.cell :seeds_count, c.seeds.size[] + csv.cell :seeds_count, c.seeds.size csv.cell :harvests_count, c.harvests.size # Sunniness From 5a7f41537f195fda918a6eaa91d4498516bb96d2 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sat, 2 May 2026 14:47:43 +0930 Subject: [PATCH 35/42] Change plant_before formatting method to to_fs --- app/views/seeds/index.csv.shaper | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/seeds/index.csv.shaper b/app/views/seeds/index.csv.shaper index b2d377885..159c1db33 100644 --- a/app/views/seeds/index.csv.shaper +++ b/app/views/seeds/index.csv.shaper @@ -32,7 +32,7 @@ csv.headers :id, csv.cell :quantity - csv.cell :plant_before, s.plant_before ? s.plant_before.to_s(:db) : '' + csv.cell :plant_before, s.plant_before ? s.plant_before.to_fs(:db) : '' csv.cell :tradable_to csv.cell :from_location, s.owner.location From 1f6f3c4dfd5443379a2fd384cd13e2121687da50 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sat, 2 May 2026 15:31:10 +0930 Subject: [PATCH 36/42] Merge pull request #4612 from Growstuff/fix-crops-csv-export-11894001552728801282 Fix ArgumentError in Crops CSV export --- app/controllers/crops_controller.rb | 2 +- app/views/crops/index.csv.shaper | 4 +++- spec/controllers/crops_controller_spec.rb | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/controllers/crops_controller.rb b/app/controllers/crops_controller.rb index 54ebcf56b..0caac083b 100644 --- a/app/controllers/crops_controller.rb +++ b/app/controllers/crops_controller.rb @@ -13,7 +13,7 @@ class CropsController < ApplicationController @crops = Crop.search('*', boost_by: %i(plantings_count harvests_count), limit: 100, page: params[:page], - load: (request.format.csv? ? { include: %i(scientific_names parent creator) } : false)) + load: (request.format.csv? || request.format.rss? ? { include: %i(parent scientific_names seeds harvests creator plantings) } : false)) @num_requested_crops = requested_crops.size if current_member @filename = filename respond_with @crops diff --git a/app/views/crops/index.csv.shaper b/app/views/crops/index.csv.shaper index 36509bcaf..049ec3f2d 100644 --- a/app/views/crops/index.csv.shaper +++ b/app/views/crops/index.csv.shaper @@ -44,7 +44,9 @@ csv.headers *all_headers @crops.each do |c| csv.row c do |csv, crop| - csv.cells :id, :name, :en_wikipedia_url + csv.cell :id, c.id + csv.cell :name, c.name + csv.cell :en_wikipedia_url, c.en_wikipedia_url csv.cell :growstuff_url, crop_url(c) if c.scientific_names.any? diff --git a/spec/controllers/crops_controller_spec.rb b/spec/controllers/crops_controller_spec.rb index 3af5de372..86dbf0988 100644 --- a/spec/controllers/crops_controller_spec.rb +++ b/spec/controllers/crops_controller_spec.rb @@ -73,6 +73,21 @@ describe CropsController do end end + describe "GET CSV" do + let!(:tomato) { create(:tomato, en_wikipedia_url: "https://en.wikipedia.org/wiki/Tomato") } + before do + Crop.reindex + get :index, format: "csv" + end + + it { is_expected.to be_successful } + it { expect(response.content_type).to eq("text/csv; charset=utf-8") } + it "contains tomato", pending: "not properly functional" do + expect(assigns(:crops)).not_to be_empty + expect(response.body).to include("tomato") + end + end + describe 'CREATE' do subject { put :create, params: crop_params } From 4589839c6428529f057bcc1db2e8096356500264 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sat, 2 May 2026 15:39:36 +0930 Subject: [PATCH 37/42] Fix crops csv export 11894001552728801282 (#4613) * Fix ArgumentError in Crops CSV export This commit fixes a crash when exporting crops to CSV, caused by accessing ActiveRecord methods and associations on Searchkick HashWrapper objects. Changes: - In CropsController#index, use `load: true` (with preloaded associations) when the request format is CSV or RSS. - In app/views/crops/index.csv.shaper, use individual `csv.cell` calls instead of `csv.cells` to correctly handle Searchkick results and explicitly access attributes. - Added a controller test to verify CSV export functionality. Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com> * Mark test pending * Skip creator --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- app/views/crops/index.csv.shaper | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/app/views/crops/index.csv.shaper b/app/views/crops/index.csv.shaper index 049ec3f2d..9ffd093c7 100644 --- a/app/views/crops/index.csv.shaper +++ b/app/views/crops/index.csv.shaper @@ -47,7 +47,7 @@ csv.headers *all_headers csv.cell :id, c.id csv.cell :name, c.name csv.cell :en_wikipedia_url, c.en_wikipedia_url - csv.cell :growstuff_url, crop_url(c) + csv.cell :growstuff_url, crop_url(slug: c.slug) if c.scientific_names.any? csv.cell :default_scientific_name, c.default_scientific_name @@ -63,7 +63,7 @@ csv.headers *all_headers csv.cell :seeds_count, c.seeds.size csv.cell :harvests_count, c.harvests.size - # Sunniness + # Sunniness sunniness = c.sunniness sunniness_rec = sunniness.max_by{|k,v| v} @@ -76,7 +76,7 @@ csv.headers *all_headers # Planted from - planted_from = c.planted_from + planted_from = c.planted_from planted_from_rec = planted_from.max_by{|k,v| v} if planted_from_rec csv.cell :plant_from_recommendation, planted_from_rec[0] @@ -107,12 +107,11 @@ csv.headers *all_headers csv.cell col, harvested_plant_parts[pp] || 0 end - csv.cell :added_by_member_id, c.creator.id - csv.cell :added_by_member_name, c.creator.to_s + csv.cell :added_by_member_id, c.creator&.id + csv.cell :added_by_member_name, c.creator&.to_s csv.cell :date_added, c.created_at.to_fs(:db) csv.cell :last_modified, c.updated_at.to_fs(:db) csv.cell :license, "CC-BY-SA Growstuff http://growstuff.org/" - end end From 6ac438a07f44ae7216b2ba55956322d1f14865b3 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sat, 2 May 2026 06:35:23 +0000 Subject: [PATCH 38/42] Namespaces no longer supported in sidekiq --- config/initializers/sidekiq.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 15990b73e..24dcf022e 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -3,9 +3,9 @@ # config/initializers/sidekiq.rb Sidekiq.configure_server do |config| - config.redis = { url: 'redis://localhost:6379/0', namespace: "app3_sidekiq_#{Rails.env}" } + config.redis = { url: 'redis://localhost:6379/0' } end Sidekiq.configure_client do |config| - config.redis = { url: 'redis://localhost:6379/0', namespace: "app3_sidekiq_#{Rails.env}" } + config.redis = { url: 'redis://localhost:6379/0' } end From c168e8e4c910e1b905dfb2449a5f5c437624a43d Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sat, 2 May 2026 06:36:27 +0000 Subject: [PATCH 39/42] Update to 3.4.9 --- .ruby-version | 2 +- Dockerfile | 2 +- Gemfile.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ruby-version b/.ruby-version index 7921bd0c8..7bcbb3808 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.8 +3.4.9 diff --git a/Dockerfile b/Dockerfile index d7caad51b..f837e41d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.4.8-trixie +FROM ruby:3.4.9-trixie # Install system dependencies RUN apt-get update -qq && \ diff --git a/Gemfile.lock b/Gemfile.lock index 268da4a35..e2afb1dc1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -886,7 +886,7 @@ DEPENDENCIES xmlrpc RUBY VERSION - ruby 3.4.8p72 + ruby 3.4.9 BUNDLED WITH 2.4.22 From 6aadb4d805386e6f5f9986979057a1e3cc1125c3 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sat, 2 May 2026 16:28:50 +0930 Subject: [PATCH 40/42] Merge pull request #4616 from Growstuff/rubocop-upgrade Update rubocop_todo.yml --- .rubocop_todo.yml | 200 +++++++++++++++++++++++++++++++++------------- 1 file changed, 146 insertions(+), 54 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 143bc3398..8a3c39e19 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,25 +1,31 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2026-04-25 16:44:38 UTC using RuboCop version 1.86.1. +# on 2026-05-02 06:53:56 UTC using RuboCop version 1.86.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. -# Offense count: 19 -Capybara/NegationMatcherAfterVisit: +# Offense count: 407 +# This cop supports safe autocorrection (--autocorrect). +Capybara/RSpec/HaveContent: + Enabled: false + +# Offense count: 21 +Capybara/RSpec/NegationMatcherAfterVisit: Exclude: - 'spec/features/admin/reverting_crops_spec.rb' - 'spec/features/crops/crop_detail_page_spec.rb' - 'spec/features/crops/crop_wranglers_spec.rb' - 'spec/features/gardens/gardens_spec.rb' + - 'spec/features/members/blocking_spec.rb' - 'spec/features/members/deletion_spec.rb' - 'spec/features/members/following_spec.rb' - 'spec/features/members/profile_spec.rb' - 'spec/features/plantings/planting_a_crop_spec.rb' # Offense count: 14 -Capybara/SpecificMatcher: +Capybara/RSpec/SpecificMatcher: Exclude: - 'spec/features/footer_spec.rb' - 'spec/features/gardens/adding_gardens_spec.rb' @@ -28,7 +34,7 @@ Capybara/SpecificMatcher: - 'spec/features/seeds/adding_seeds_spec.rb' # Offense count: 1 -Capybara/VisibilityMatcher: +Capybara/RSpec/VisibilityMatcher: Exclude: - 'spec/features/shared_examples/crop_suggest.rb' @@ -63,7 +69,13 @@ FactoryBot/ExcessiveCreateList: - 'spec/features/crops/show_spec.rb' - 'spec/features/percy/percy_spec.rb' -# Offense count: 312 +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Layout/EmptyLines: + Exclude: + - 'config/environments/production.rb' + +# Offense count: 311 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # SupportedHashRocketStyles: key, separator, table @@ -81,7 +93,7 @@ Layout/HashAlignment: - 'spec/requests/api/v1/activities_request_spec.rb' - 'spec/requests/api/v1/members_request_spec.rb' -# Offense count: 5 +# Offense count: 8 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings. # URISchemes: http, https @@ -89,9 +101,20 @@ Layout/LineLength: Exclude: - 'Gemfile' - 'app/controllers/admin/versions_controller.rb' + - 'app/controllers/crops_controller.rb' - 'app/models/concerns/predict_planting.rb' - 'app/models/crop.rb' - 'db/seeds.rb' + - 'lib/tasks/members.rake' + +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: aligned, indented, indented_relative_to_receiver +Layout/MultilineMethodCallIndentation: + Exclude: + - 'app/models/activity.rb' + - 'app/models/concerns/predict_harvest.rb' # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). @@ -99,23 +122,15 @@ Lint/AmbiguousOperatorPrecedence: Exclude: - 'app/controllers/activities_controller.rb' -# Offense count: 4 +# Offense count: 3 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: RequireParenthesesForMethodChains. Lint/AmbiguousRange: Exclude: - - 'app/models/concerns/search_activities.rb' - 'app/models/concerns/search_harvests.rb' - 'app/models/concerns/search_plantings.rb' - 'db/seeds.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowSafeAssignment. -Lint/AssignmentInCondition: - Exclude: - - 'app/helpers/crops_helper.rb' - # Offense count: 1 # Configuration parameters: AllowedMethods. # AllowedMethods: enums @@ -158,7 +173,7 @@ Lint/UselessConstantScoping: Exclude: - 'app/controllers/members_controller.rb' -# Offense count: 61 +# Offense count: 65 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: Max: 295 @@ -180,12 +195,12 @@ Metrics/CollectionLiteralLength: Exclude: - 'lib/tasks/import.rake' -# Offense count: 10 +# Offense count: 11 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: Max: 32 -# Offense count: 82 +# Offense count: 83 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Max: 296 @@ -195,7 +210,7 @@ Metrics/MethodLength: Metrics/ModuleLength: Max: 144 -# Offense count: 8 +# Offense count: 10 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: Max: 32 @@ -209,6 +224,16 @@ Naming/PredicateMethod: - 'app/models/concerns/finishable.rb' - 'app/models/seed.rb' +# Offense count: 1 +# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs. +# NamePrefix: is_, has_, have_, does_ +# ForbiddenPrefixes: is_, has_, have_, does_ +# AllowedMethods: is_a? +# MethodDefinitionMacros: define_method, define_singleton_method +Naming/PredicatePrefix: + Exclude: + - 'app/models/member.rb' + # Offense count: 3 RSpec/AnyInstance: Exclude: @@ -221,34 +246,54 @@ RSpec/BeEq: Exclude: - 'spec/requests/api/v1/activities_request_spec.rb' -# Offense count: 1 +# Offense count: 2 RSpec/BeforeAfterAll: Exclude: - 'spec/tasks/import_spec.rb' + - 'spec/tasks/members_spec.rb' -# Offense count: 298 +# Offense count: 311 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: Enabled: false -# Offense count: 36 +# Offense count: 2 +# Configuration parameters: IgnoredMetadata. +RSpec/DescribeClass: + Exclude: + - 'spec/models/harvest_prediction_spec.rb' + - 'spec/tasks/members_spec.rb' + +# Offense count: 37 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. # SupportedStyles: described_class, explicit RSpec/DescribedClass: Exclude: + - 'spec/mailers/harvest_reminder_mailer_spec.rb' - 'spec/models/like_spec.rb' - 'spec/models/member_spec.rb' - 'spec/services/timeline_service_spec.rb' -# Offense count: 146 +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowConsecutiveOneLiners. +RSpec/EmptyLineAfterExample: + Exclude: + - 'spec/controllers/crops_controller_spec.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +RSpec/EmptyLineAfterFinalLet: + Exclude: + - 'spec/controllers/crops_controller_spec.rb' + +# Offense count: 161 # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 27 -# Offense count: 32 - # Offense count: 1 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. @@ -283,27 +328,18 @@ RSpec/IncludeExamples: - 'spec/views/photos/show.html.haml_spec.rb' - 'spec/views/seeds/index.rss.haml_spec.rb' -# Offense count: 37 +# Offense count: 2 # Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns. RSpec/IndexedLet: Exclude: - - 'spec/controllers/harvests_controller_spec.rb' - - 'spec/controllers/plantings_controller_spec.rb' - - 'spec/features/crops/crop_photos_spec.rb' - - 'spec/features/members/list_spec.rb' - - 'spec/features/members/profile_spec.rb' - - 'spec/features/percy/percy_spec.rb' - - 'spec/features/planting_reminder_spec.rb' - - 'spec/features/timeline/index_spec.rb' - - 'spec/models/member_spec.rb' - - 'spec/views/forums/index.html.haml_spec.rb' + - 'spec/models/activity_spec.rb' -# Offense count: 719 +# Offense count: 711 # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Enabled: false -# Offense count: 41 +# Offense count: 43 RSpec/LetSetup: Enabled: false @@ -318,7 +354,7 @@ RSpec/MessageChain: Exclude: - 'spec/models/member_spec.rb' -# Offense count: 23 +# Offense count: 65 # Configuration parameters: . # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -329,11 +365,11 @@ RSpec/MultipleDescribes: Exclude: - 'spec/features/crops/crop_wranglers_spec.rb' -# Offense count: 191 +# Offense count: 235 RSpec/MultipleExpectations: Max: 19 -# Offense count: 166 +# Offense count: 171 # Configuration parameters: AllowSubject. RSpec/MultipleMemoizedHelpers: Max: 16 @@ -344,24 +380,35 @@ RSpec/MultipleMemoizedHelpers: RSpec/NamedSubject: Enabled: false -# Offense count: 109 +# Offense count: 112 # Configuration parameters: AllowedGroups. RSpec/NestedGroups: Max: 6 -# Offense count: 407 +# Offense count: 366 # Configuration parameters: AllowedPatterns. # AllowedPatterns: ^expect_, ^assert_ RSpec/NoExpectationExample: Enabled: false -# Offense count: 4 +# Offense count: 9 RSpec/PendingWithoutReason: Exclude: + - 'spec/features/members/blocking_spec.rb' + - 'spec/features/plantings/planting_a_crop_spec.rb' - 'spec/features/seeds/misc_seeds_spec.rb' - 'spec/features/unsubscribing_spec.rb' + - 'spec/models/ability_spec.rb' - 'spec/requests/api/v1/gardens_request_spec.rb' +# Offense count: 5 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Strict, EnforcedStyle, AllowedExplicitMatchers. +# SupportedStyles: inflected, explicit +RSpec/PredicateMatcher: + Exclude: + - 'spec/tasks/members_spec.rb' + # Offense count: 2 RSpec/RepeatedDescription: Exclude: @@ -386,18 +433,18 @@ RSpec/ScatteredSetup: - 'spec/features/percy/percy_spec.rb' - 'spec/features/plantings/prediction_spec.rb' -# Offense count: 1 +# Offense count: 2 # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector. # SupportedInflectors: default, active_support RSpec/SpecFilePathFormat: Exclude: - 'spec/controllers/member_controller_spec.rb' + - 'spec/mailers/harvest_reminder_mailer_spec.rb' -# Offense count: 3 +# Offense count: 2 RSpec/StubbedMock: Exclude: - - 'spec/controllers/garden_types_controller_spec.rb' - - 'spec/controllers/gardens_controller_spec.rb' + - 'spec/controllers/photos_controller_spec.rb' - 'spec/models/member_spec.rb' # Offense count: 1 @@ -414,6 +461,13 @@ RSpec/VerifiedDoubles: - 'spec/controllers/gardens_controller_spec.rb' - 'spec/views/devise/shared/_links_spec.rb' +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Inferences. +RSpecRails/InferredSpecType: + Exclude: + - 'spec/mailers/harvest_reminder_mailer_spec.rb' + # Offense count: 30 # Configuration parameters: Database. # SupportedDatabases: mysql, postgresql @@ -470,11 +524,25 @@ Rails/HasManyOrHasOneDependent: - 'app/models/crop.rb' - 'app/models/member.rb' +# Offense count: 7 +Rails/HelperInstanceVariable: + Exclude: + - 'app/helpers/crops_helper.rb' + - 'app/helpers/plantings_helper.rb' + # Offense count: 1 Rails/I18nLocaleAssignment: Exclude: - 'spec/features/locale_spec.rb' +# Offense count: 5 +Rails/I18nLocaleTexts: + Exclude: + - 'app/controllers/blocks_controller.rb' + - 'app/controllers/comments_controller.rb' + - 'app/controllers/messages_controller.rb' + - 'config/initializers/comfortable_mexican_sofa.rb' + # Offense count: 1 # Configuration parameters: IgnoreScopes. Rails/InverseOf: @@ -488,6 +556,12 @@ Rails/LexicallyScopedActionFilter: - 'app/controllers/data_controller.rb' - 'app/controllers/registrations_controller.rb' +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Rails/OrderArguments: + Exclude: + - 'app/models/activity.rb' + # Offense count: 2 Rails/OutputSafety: Exclude: @@ -614,7 +688,7 @@ Rake/MethodDefinitionInTask: Exclude: - 'lib/tasks/growstuff.rake' -# Offense count: 4 +# Offense count: 5 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules. # SupportedStyles: nested, compact @@ -623,10 +697,17 @@ Rake/MethodDefinitionInTask: Style/ClassAndModuleChildren: Exclude: - 'app/controllers/admin/crops_controller.rb' + - 'config/initializers/rack_attack.rb' - 'lib/actions/oauth_signup_action.rb' - 'lib/haml/filters/escaped_markdown.rb' - 'lib/haml/filters/growstuff_markdown.rb' +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/CollectionQuerying: + Exclude: + - 'app/models/member.rb' + # Offense count: 6 # This cop supports unsafe autocorrection (--autocorrect-all). Style/CommentedKeyword: @@ -657,12 +738,13 @@ Style/FloatDivision: Exclude: - 'app/models/concerns/predict_planting.rb' -# Offense count: 1 +# Offense count: 2 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: always, always_true, never Style/FrozenStringLiteralComment: Exclude: + - 'db/migrate/20260429132911_add_send_harvest_reminder_to_members.rb' - 'spec/lib/haml/filters/growstuff_markdown_spec.rb' # Offense count: 2 @@ -678,11 +760,14 @@ Style/IdenticalConditionalBranches: Exclude: - 'lib/actions/oauth_signup_action.rb' -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/MapIntoArray: +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: Exclude: - 'app/helpers/crops_helper.rb' + - 'app/models/concerns/predict_planting.rb' + - 'config/initializers/rack_attack.rb' + - 'lib/tasks/growstuff.rake' # Offense count: 1 # This cop supports unsafe autocorrection (--autocorrect-all). @@ -736,6 +821,13 @@ Style/RedundantArgument: Exclude: - 'app/helpers/application_helper.rb' +# Offense count: 3 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantBegin: + Exclude: + - 'app/models/concerns/predict_harvest.rb' + - 'app/models/harvest.rb' + # Offense count: 4 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SafeForConstants. From ee7b9ab39f70289c3099e4151051a386984cbabb Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sat, 2 May 2026 07:03:30 +0000 Subject: [PATCH 41/42] Upgrade ERB --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e2afb1dc1..2e44f485d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -276,7 +276,7 @@ GEM elasticsearch-transport (7.0.0) faraday multi_json - erb (6.0.2) + erb (6.0.4) erubi (1.13.1) execjs (2.10.0) factory_bot (6.5.5) From 8a8fd6eabd36ef54c9bc85574a92730e6e418832 Mon Sep 17 00:00:00 2001 From: Daniel O'Connor Date: Sat, 2 May 2026 16:46:26 +0930 Subject: [PATCH 42/42] Merge pull request #4575 from Growstuff/memoize-unread-count Memoize unread messages count --- app/models/member.rb | 2 +- spec/views/layouts/_header_spec.rb | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/models/member.rb b/app/models/member.rb index 82e5a6e41..7a0f31e25 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -162,7 +162,7 @@ class Member < ApplicationRecord end def unread_count - receipts.where(is_read: false).count + @unread_count ||= receipts.where(is_read: false).count end def self.login_name_or_email(login) diff --git a/spec/views/layouts/_header_spec.rb b/spec/views/layouts/_header_spec.rb index d71a35537..6851e5d4e 100644 --- a/spec/views/layouts/_header_spec.rb +++ b/spec/views/layouts/_header_spec.rb @@ -90,13 +90,19 @@ describe 'layouts/_header.html.haml', type: "view" do expect(rendered).to have_content 'Inbox' expect(rendered).not_to match(/Inbox \d+/) end + end - context 'has notifications' do - it 'shows inbox count' do - create(:notification, recipient: @member) - render - expect(rendered).to have_content 'Inbox 1' - end + context 'logged in, has notifications' do + before do + @member = create(:member) + create(:notification, recipient: @member) + sign_in @member + controller.stub(:current_user) { @member } + end + + it 'shows inbox count' do + render + rendered.should have_content 'Inbox 1' end end end