From 11b63edf84095d5ccbc1d2f9adab0f722a637e48 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 4 Mar 2026 11:55:10 -0500 Subject: [PATCH 1/4] Make workshops commentable with admin-only UI and fix comment field bugs - Add polymorphic comments association to Workshop model - Add nested comments to workshop edit form (admin-only, persisted records) - Wire up comments controller, routes, and strong params for workshops - Fix created_by/updated_by not persisting on nested comment attributes - Remove debug "test" text from comment fields partial - Show "--" chip fallback for unknown comment authors - Fix two-user initials chips to split 50% width evenly - Wrap images section in collapsible toggle (expanded by default) - Add workshop polymorphic test and factory trait Co-Authored-By: Claude Opus 4.6 --- app/controllers/comments_controller.rb | 2 + app/controllers/workshops_controller.rb | 9 ++- app/models/workshop.rb | 2 + app/views/comments/_comment_fields.html.erb | 18 ++---- app/views/workshops/_form.html.erb | 64 ++++++++++++++++++++- config/routes.rb | 4 +- spec/factories/comments.rb | 4 ++ spec/models/comment_spec.rb | 7 +++ 8 files changed, 94 insertions(+), 16 deletions(-) diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 09407ebce..b0c9cfa0b 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -42,6 +42,8 @@ def set_commentable @commentable = Organization.find(params[:organization_id]) elsif params[:event_registration_id] @commentable = EventRegistration.find(params[:event_registration_id]) + elsif params[:workshop_id] + @commentable = Workshop.find(params[:workshop_id]) else redirect_to root_path, alert: "Invalid commentable resource" end diff --git a/app/controllers/workshops_controller.rb b/app/controllers/workshops_controller.rb index 03ac9f226..887fca99a 100644 --- a/app/controllers/workshops_controller.rb +++ b/app/controllers/workshops_controller.rb @@ -153,8 +153,12 @@ def update authorize! @workshop success = false + @workshop.assign_attributes(workshop_params) + @workshop.comments.select(&:new_record?).each { |c| c.created_by = current_user; c.updated_by = current_user } + @workshop.comments.select(&:changed?).each { |c| c.updated_by = current_user } + Workshop.transaction do - if @workshop.update(workshop_params) + if @workshop.save assign_associations(@workshop) success = true end @@ -288,7 +292,8 @@ def workshop_params gallery_assets_attributes: [ :id, :file, :_destroy ], workshop_series_children_attributes: [ :id, :workshop_child_id, :workshop_parent_id, :theme_name, :series_description, :series_description_spanish, - :position, :_destroy ] + :position, :_destroy ], + comments_attributes: [ :id, :body ] ) end end diff --git a/app/models/workshop.rb b/app/models/workshop.rb index 86a2f645d..7f86b1cba 100644 --- a/app/models/workshop.rb +++ b/app/models/workshop.rb @@ -28,6 +28,7 @@ def self.mentionable_rich_text_fields belongs_to :workshop_idea, optional: true has_many :bookmarks, as: :bookmarkable, dependent: :destroy + has_many :comments, -> { newest_first }, as: :commentable, dependent: :destroy has_many :categorizable_items, dependent: :destroy, inverse_of: :categorizable, as: :categorizable has_many :quotable_item_quotes, as: :quotable, dependent: :destroy has_many :associated_resources, class_name: "Resource", foreign_key: "workshop_id", dependent: :restrict_with_error @@ -135,6 +136,7 @@ def self.mentionable_rich_text_fields reject_if: proc { |attributes| attributes["workshop_child_id"].blank? }, allow_destroy: true accepts_nested_attributes_for :workshop_variations, reject_if: proc { |object| object.nil? } + accepts_nested_attributes_for :comments, reject_if: proc { |attrs| attrs["body"].blank? } # Scopes diff --git a/app/views/comments/_comment_fields.html.erb b/app/views/comments/_comment_fields.html.erb index fbf81b8b3..79d819280 100644 --- a/app/views/comments/_comment_fields.html.erb +++ b/app/views/comments/_comment_fields.html.erb @@ -37,14 +37,10 @@ <% if show_updated %> - <% if created_initials.present? %> - <%= created_initials %> - <% else %> - test - <% end %> - <%= updated_initials %> + <%= created_initials.presence || "--" %> + <%= updated_initials.presence || "--" %> <% else %> - <%= truncate(created_first_name.presence || "Unknown", length: 10, omission: "..") %> + <%= truncate(created_first_name.presence || "--", length: 10, omission: "..") %> <% end %> @@ -57,12 +53,10 @@ <% if show_updated %> - <% if created_initials.present? %> - <%= created_initials %> - <% end %> - <%= updated_initials %> + <%= created_initials.presence || "--" %> + <%= updated_initials.presence || "--" %> <% else %> - <%= truncate(created_first_name.to_s, length: 10, omission: "..") %> + <%= truncate(created_first_name.presence || "--", length: 10, omission: "..") %> <% end %>
diff --git a/app/views/workshops/_form.html.erb b/app/views/workshops/_form.html.erb index e84f6f791..a3a6bd400 100644 --- a/app/views/workshops/_form.html.erb +++ b/app/views/workshops/_form.html.erb @@ -425,9 +425,71 @@
+ +
+ + +
+ <%= render "shared/form_image_fields", f: f, include_primary_asset: true %> +
+
- <%= render "shared/form_image_fields", f: f, include_primary_asset: true %> + <% if f.object.persisted? %> + <% if allowed_to?(:manage?, Comment) %> +
+
Comments
+ +
+ <%= f.simple_fields_for :comments do |cf| %> + <%= render "comments/comment_fields", f: cf %> + <% end %> + +
+ + + <%= link_to_add_association "➕ Add Comment", + f, + :comments, + partial: "comments/comment_fields", + class: "btn btn-secondary-outline" %> +
+
+ <% end %> + <% end %> +
<% if allowed_to?(:destroy?, f.object) %> <%= link_to "Delete", @workshop, class: "btn btn-danger-outline", diff --git a/config/routes.rb b/config/routes.rb index bbd10c7d9..9256c4ce5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -168,7 +168,9 @@ resources :workshop_log_creation_wizard resources :workshop_variation_ideas resources :workshop_variations - resources :workshops + resources :workshops do + resources :comments, only: [ :index, :create ] + end resources :workshop_mentions, only: [ :index ] resources :resource_mentions, only: [ :index ] diff --git a/spec/factories/comments.rb b/spec/factories/comments.rb index 0283afbbc..094db6d61 100644 --- a/spec/factories/comments.rb +++ b/spec/factories/comments.rb @@ -12,5 +12,9 @@ trait :for_event_registration do association :commentable, factory: :event_registration end + + trait :for_workshop do + association :commentable, factory: :workshop + end end end diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb index 75ac95c66..1cc254bb4 100644 --- a/spec/models/comment_spec.rb +++ b/spec/models/comment_spec.rb @@ -42,6 +42,13 @@ expect(comment.commentable).to eq(event_registration) expect(event_registration.comments).to include(comment) end + + it 'can be associated with a Workshop' do + workshop = create(:workshop) + comment = create(:comment, commentable: workshop, body: "Workshop comment") + expect(comment.commentable).to eq(workshop) + expect(workshop.comments).to include(comment) + end end describe 'scopes' do From 0080cfd9e610647aaedd0cd00c0ad73ee2fd4cf4 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 4 Mar 2026 19:52:06 -0500 Subject: [PATCH 2/4] Fix comment updated_by overwrite and ensure hidden id on nested fields - Change `changed?` to `persisted? && body_changed?` in all 5 commentable controllers to prevent updated_by being set on every save - Add explicit hidden id field for persisted comments to prevent created_by overwrite from missing ids in nested attributes - Add rounded-md and mt-6 to workshop comments section Co-Authored-By: Claude Opus 4.6 --- app/controllers/event_registrations_controller.rb | 2 +- app/controllers/organizations_controller.rb | 2 +- app/controllers/people_controller.rb | 2 +- app/controllers/users_controller.rb | 2 +- app/controllers/workshops_controller.rb | 2 +- app/views/comments/_comment_fields.html.erb | 1 + app/views/workshops/_form.html.erb | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/controllers/event_registrations_controller.rb b/app/controllers/event_registrations_controller.rb index 144178bd7..28bce31eb 100644 --- a/app/controllers/event_registrations_controller.rb +++ b/app/controllers/event_registrations_controller.rb @@ -75,7 +75,7 @@ def update authorize! @event_registration @event_registration.assign_attributes(event_registration_update_params) @event_registration.comments.select(&:new_record?).each { |c| c.created_by = current_user; c.updated_by = current_user } - @event_registration.comments.select(&:changed?).each { |c| c.updated_by = current_user } + @event_registration.comments.select { |c| c.persisted? && c.body_changed? }.each { |c| c.updated_by = current_user } if @event_registration.save respond_to do |format| diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 7ed3c28b8..8b4af466c 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -100,7 +100,7 @@ def update authorize! @organization @organization.assign_attributes(organization_params) @organization.comments.select(&:new_record?).each { |c| c.created_by = current_user; c.updated_by = current_user } - @organization.comments.select(&:changed?).each { |c| c.updated_by = current_user } + @organization.comments.select { |c| c.persisted? && c.body_changed? }.each { |c| c.updated_by = current_user } if @organization.save assign_associations(@organization) if params.dig(:organization, :category_ids) diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index b8d7128bd..629f36773 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -134,7 +134,7 @@ def update @person.assign_attributes(person_params) @person.comments.select(&:new_record?).each { |c| c.created_by = current_user; c.updated_by = current_user } - @person.comments.select(&:changed?).each { |c| c.updated_by = current_user } + @person.comments.select { |c| c.persisted? && c.body_changed? }.each { |c| c.updated_by = current_user } if @person.save assign_associations(@person) if params.dig(:person, :category_ids) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 966402b7f..d5b1093e1 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -126,7 +126,7 @@ def update @user.assign_attributes(user_params.except(:password, :password_confirmation)) @user.updated_by = current_user @user.comments.select(&:new_record?).each { |c| c.created_by = current_user; c.updated_by = current_user } - @user.comments.select(&:changed?).each { |c| c.updated_by = current_user } + @user.comments.select { |c| c.persisted? && c.body_changed? }.each { |c| c.updated_by = current_user } # Suppress Devise's automatic reconfirmation email so the interstitial can control it @user.skip_confirmation_notification! diff --git a/app/controllers/workshops_controller.rb b/app/controllers/workshops_controller.rb index 887fca99a..13c86bde0 100644 --- a/app/controllers/workshops_controller.rb +++ b/app/controllers/workshops_controller.rb @@ -155,7 +155,7 @@ def update @workshop.assign_attributes(workshop_params) @workshop.comments.select(&:new_record?).each { |c| c.created_by = current_user; c.updated_by = current_user } - @workshop.comments.select(&:changed?).each { |c| c.updated_by = current_user } + @workshop.comments.select { |c| c.persisted? && c.body_changed? }.each { |c| c.updated_by = current_user } Workshop.transaction do if @workshop.save diff --git a/app/views/comments/_comment_fields.html.erb b/app/views/comments/_comment_fields.html.erb index 79d819280..423bf07b5 100644 --- a/app/views/comments/_comment_fields.html.erb +++ b/app/views/comments/_comment_fields.html.erb @@ -30,6 +30,7 @@ <% updated_color = show_updated ? label_colors[(updated_user.id || 0) % label_colors.length] : nil %> <% current_color = label_colors[(current_user&.id || 0) % label_colors.length] %>
+ <%= f.hidden_field :id if f.object.persisted? %> <% if f.object.persisted? %>
diff --git a/app/views/workshops/_form.html.erb b/app/views/workshops/_form.html.erb index a3a6bd400..bf43f3ddf 100644 --- a/app/views/workshops/_form.html.erb +++ b/app/views/workshops/_form.html.erb @@ -461,7 +461,7 @@ <% if f.object.persisted? %> <% if allowed_to?(:manage?, Comment) %> -
+
Comments
Date: Thu, 5 Mar 2026 07:03:21 -0500 Subject: [PATCH 3/4] Fix race condition in event registration Turbo test Wait for "View Registration" link to appear before querying the database, ensuring the Turbo stream response has committed the registration record. Co-Authored-By: Claude Opus 4.6 --- spec/system/events_show_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/system/events_show_spec.rb b/spec/system/events_show_spec.rb index 5451c4342..cec641a5a 100644 --- a/spec/system/events_show_spec.rb +++ b/spec/system/events_show_spec.rb @@ -426,6 +426,7 @@ expect(page).not_to have_button("Register") # "View Registration" is a clickable link to the registration show page + expect(page).to have_link("View Registration") registration = EventRegistration.last expect(page).to have_link("View Registration", href: registration_ticket_path(registration.slug)) end From 09f8e94aadba033f6458e198122ffec7af82f7f2 Mon Sep 17 00:00:00 2001 From: maebeale Date: Thu, 5 Mar 2026 11:19:45 -0500 Subject: [PATCH 4/4] Fix workshop RecordNotUnique test to stub save instead of update The update action now uses assign_attributes + save instead of update, so the test stub needs to match. Co-Authored-By: Claude Opus 4.6 --- spec/requests/workshops_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/workshops_spec.rb b/spec/requests/workshops_spec.rb index 254923873..655a429b1 100644 --- a/spec/requests/workshops_spec.rb +++ b/spec/requests/workshops_spec.rb @@ -128,7 +128,7 @@ it "handles RecordNotUnique gracefully" do workshop = create(:workshop) - allow_any_instance_of(Workshop).to receive(:update).and_raise( + allow_any_instance_of(Workshop).to receive(:save).and_raise( ActiveRecord::RecordNotUnique.new("Duplicate entry") )