From c4b1ef2f5859a212d6ba260b7a66386d8d096686 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 1 Mar 2026 21:18:59 -0500 Subject: [PATCH 1/2] Add bust_cache param to fix stale featured workshops on home page Featured workshop IDs are cached for 1 year and the before_save invalidation can race with the save transaction, leaving stale data. Visiting /?bust_cache=true now clears and repopulates the cache. Co-Authored-By: Claude Opus 4.6 --- README.md | 12 +++++ app/controllers/home/workshops_controller.rb | 4 +- app/views/home/_workshops.html.erb | 2 +- spec/requests/home/workshops_spec.rb | 47 ++++++++++++++++++++ 4 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 spec/requests/home/workshops_spec.rb diff --git a/README.md b/README.md index 7816e5bee..16bee54e7 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,18 @@ This is a Rails 8.1.0 application built with: For detailed setup and development instructions, please see our [CONTRIBUTING.md](CONTRIBUTING.md) guide. +## Production Maintenance + +### Featured workshops not showing on the home page + +Featured workshop IDs are cached for up to 1 year. The cache auto-invalidates when a workshop's `featured`, `publicly_featured`, or `published` flags change, but stale data can persist if the invalidation races with the save transaction. To force a refresh, visit: + +``` +/?bust_cache=true +``` + +This clears and repopulates the cache for all users. + ## Orphaned Reports When users are deleted from the system, their reports are automatically assigned to a special diff --git a/app/controllers/home/workshops_controller.rb b/app/controllers/home/workshops_controller.rb index f60262130..90984c459 100644 --- a/app/controllers/home/workshops_controller.rb +++ b/app/controllers/home/workshops_controller.rb @@ -2,12 +2,14 @@ module Home class WorkshopsController < ApplicationController def index authorize! :home + Rails.cache.delete("featured_and_publicly_featured_workshop_ids") if params[:bust_cache] == "true" + ids = Rails.cache.fetch("featured_and_publicly_featured_workshop_ids", expires_in: 1.year) do Workshop.featured_or_publicly_featured.pluck(:id) end base_scope = Workshop.includes(:windows_type, primary_asset: { file_attachment: :blob }, gallery_assets: { file_attachment: :blob }) - .where(id: ids) + .where(id: ids) @workshops = authorized_scope(base_scope, with: HomePolicy).decorate @workshops = @workshops.sort { |x, y| Date.parse(y.date) <=> Date.parse(x.date) } diff --git a/app/views/home/_workshops.html.erb b/app/views/home/_workshops.html.erb index 106743d11..9376401e7 100644 --- a/app/views/home/_workshops.html.erb +++ b/app/views/home/_workshops.html.erb @@ -6,7 +6,7 @@ title: title, subtitle: "Spotlights from our curriculum" %> - <%= turbo_frame_tag "home_workshops", src: home_workshops_path do %> + <%= turbo_frame_tag "home_workshops", src: home_workshops_path(bust_cache: params[:bust_cache].presence) do %> <%= render "workshops_cards_skeleton" %> <% end %> diff --git a/spec/requests/home/workshops_spec.rb b/spec/requests/home/workshops_spec.rb new file mode 100644 index 000000000..5c98dcfa9 --- /dev/null +++ b/spec/requests/home/workshops_spec.rb @@ -0,0 +1,47 @@ +require "rails_helper" + +RSpec.describe "/home/workshops", type: :request do + let(:user) { create(:user) } + let!(:windows_type) { create(:windows_type) } + + before { sign_in user } + + describe "GET /home/workshops" do + it "returns featured workshops" do + workshop = create(:workshop, :published, featured: true, windows_type: windows_type) + + get home_workshops_path + + expect(response).to have_http_status(:ok) + expect(response.body).to include(workshop.title) + end + + it "does not return unfeatured workshops" do + create(:workshop, :published, featured: false, windows_type: windows_type) + + get home_workshops_path + + expect(response).to have_http_status(:ok) + expect(response.body).to include("No workshops available right now.") + end + + context "with bust_cache=true" do + it "clears and repopulates the featured workshop cache" do + # Prime the cache with no featured workshops + get home_workshops_path + expect(response.body).to include("No workshops available right now.") + + # Feature a workshop after the cache is set + create(:workshop, :published, featured: true, windows_type: windows_type) + + # Without bust_cache, stale cache returns no workshops + get home_workshops_path + expect(response.body).to include("No workshops available right now.") + + # With bust_cache=true, the cache is cleared and workshops appear + get home_workshops_path, params: { bust_cache: "true" } + expect(response.body).not_to include("No workshops available right now.") + end + end + end +end From 648372421d53ef1fb8241e2b9c963918910ec910 Mon Sep 17 00:00:00 2001 From: maebeale Date: Sun, 1 Mar 2026 21:21:12 -0500 Subject: [PATCH 2/2] Restrict bust_cache to site admins Co-Authored-By: Claude Opus 4.6 --- app/controllers/home/workshops_controller.rb | 4 +++- spec/requests/home/workshops_spec.rb | 20 +++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/controllers/home/workshops_controller.rb b/app/controllers/home/workshops_controller.rb index 90984c459..7d61f9a26 100644 --- a/app/controllers/home/workshops_controller.rb +++ b/app/controllers/home/workshops_controller.rb @@ -2,7 +2,9 @@ module Home class WorkshopsController < ApplicationController def index authorize! :home - Rails.cache.delete("featured_and_publicly_featured_workshop_ids") if params[:bust_cache] == "true" + if params[:bust_cache] == "true" && current_user&.super_user? + Rails.cache.delete("featured_and_publicly_featured_workshop_ids") + end ids = Rails.cache.fetch("featured_and_publicly_featured_workshop_ids", expires_in: 1.year) do Workshop.featured_or_publicly_featured.pluck(:id) diff --git a/spec/requests/home/workshops_spec.rb b/spec/requests/home/workshops_spec.rb index 5c98dcfa9..5324604a0 100644 --- a/spec/requests/home/workshops_spec.rb +++ b/spec/requests/home/workshops_spec.rb @@ -26,22 +26,28 @@ end context "with bust_cache=true" do - it "clears and repopulates the featured workshop cache" do + let(:admin) { create(:user, :admin) } + + before do # Prime the cache with no featured workshops get home_workshops_path - expect(response.body).to include("No workshops available right now.") - # Feature a workshop after the cache is set create(:workshop, :published, featured: true, windows_type: windows_type) + end - # Without bust_cache, stale cache returns no workshops - get home_workshops_path - expect(response.body).to include("No workshops available right now.") + it "clears the cache for admins" do + sign_in admin - # With bust_cache=true, the cache is cleared and workshops appear get home_workshops_path, params: { bust_cache: "true" } + expect(response.body).not_to include("No workshops available right now.") end + + it "does not clear the cache for non-admins" do + get home_workshops_path, params: { bust_cache: "true" } + + expect(response.body).to include("No workshops available right now.") + end end end end