diff --git a/MIGRATION_NOTES.md b/MIGRATION_NOTES.md new file mode 100644 index 000000000..680788df7 --- /dev/null +++ b/MIGRATION_NOTES.md @@ -0,0 +1,94 @@ +# Migration Notes for Issue #849 + +## Overview +This PR converts the `quotable_item_quotes` join table pattern to a direct polymorphic relationship between `quotes` and quotable items (workshops, reports, workshop_logs). + +## Database Migrations + +Two migrations have been added: + +### 1. Add Quotable Columns to Quotes (20260215071524) +```ruby +add_reference :quotes, :quotable, polymorphic: true, index: true +``` +This adds `quotable_id` and `quotable_type` columns to the `quotes` table. + +### 2. Migrate Data (20260215071525) +```sql +UPDATE quotes +INNER JOIN quotable_item_quotes ON quotes.id = quotable_item_quotes.quote_id +SET quotes.quotable_id = quotable_item_quotes.quotable_id, + quotes.quotable_type = quotable_item_quotes.quotable_type +``` +This copies the associations from the join table to the quotes table. + +## Running Migrations + +```bash +# Development +bundle exec rails db:migrate + +# Docker +docker compose exec web bundle exec rails db:migrate + +# Or using mise +mise docker-exec rails db:migrate +``` + +## Rollback Plan + +The data migration is reversible: +```bash +bundle exec rails db:rollback STEP=2 +``` + +This will: +1. Clear the `quotable_id` and `quotable_type` columns on quotes +2. Remove the columns from the quotes table + +The original `quotable_item_quotes` table data is preserved and can be used to restore the relationship. + +## Testing the Changes + +```bash +# Run model tests +bundle exec rspec spec/models/quote_spec.rb +bundle exec rspec spec/models/workshop_spec.rb +bundle exec rspec spec/models/report_spec.rb + +# Run all tests +bundle exec rspec +``` + +## Future Work + +After verifying this works in production: +- [ ] Remove the `quotable_item_quotes` table with a new migration +- [ ] Archive or remove the `QuotableItemQuote` model +- [ ] Remove the `quotable_item_quotes` factory + +## Changes Made + +### Models +- **Quote**: Added `belongs_to :quotable, polymorphic: true, optional: true` +- **Workshop**: Changed from `has_many :quotable_item_quotes` to `has_many :quotes, as: :quotable` +- **Report**: Changed from `has_many :quotable_item_quotes` to `has_many :quotes, as: :quotable` +- **Report**: Updated nested attributes from `quotable_item_quotes` to `quotes` + +### Controllers +- **WorkshopLogsController**: Simplified form building logic to use direct quote associations +- **WorkshopLogsController**: Updated permitted params to use `quotes_attributes` + +### Views +- **workshop_logs/_form.html.erb**: Updated to use `:quotes` instead of `:quotable_item_quotes` +- **workshop_logs/_quote_fields.html.erb**: New simplified partial for quote fields +- **quotes/index.html.erb**: Updated to use direct `quote.quotable` instead of iterating through `quotable_item_quotes` +- **quotes/show.html.erb**: Updated to use direct `@quote.quotable` instead of iterating through `quotable_item_quotes` + +### Decorators +- **QuoteDecorator**: Simplified to use `object.quotable` instead of `object.quotable_item_quotes.last&.quotable` + +### Tests +- **spec/models/quote_spec.rb**: Added tests for polymorphic association +- **spec/models/workshop_spec.rb**: Updated association tests +- **spec/models/report_spec.rb**: Updated association tests diff --git a/app/controllers/workshop_logs_controller.rb b/app/controllers/workshop_logs_controller.rb index 4a4952bc6..af4f919eb 100644 --- a/app/controllers/workshop_logs_controller.rb +++ b/app/controllers/workshop_logs_controller.rb @@ -143,17 +143,8 @@ def set_form_variables .order(title: :asc) # Build one blank quote if none exists - @workshop_log.quotable_item_quotes.each do |qiq| - qiq.build_quote unless qiq.quote - qiq.quotable = @workshop_log - end - - # Always build at least one new blank quotable_item_quote - if @workshop_log.quotable_item_quotes.empty? - qiq = @workshop_log.quotable_item_quotes.build - qiq.build_quote - qiq.quotable = @workshop_log - end + # Always build at least one new blank quote + @workshop_log.quotes.build if @workshop_log.quotes.empty? # @sectors = Sector.published.map{ |si| [ si.id, si.name ] } # @files = MediaFile.where(["workshop_log_id = ?", @workshop_log.id]) @@ -199,12 +190,7 @@ def workshop_log_params :children_ongoing, :children_first_time, :teens_ongoing, :teens_first_time, :adults_ongoing, :adults_first_time, :owner_id, :owner_type, :user_id, :organization_id, :date, :workshop_name, :workshop_id, :windows_type_id, :other_description, :external_workshop_title, # :user, - quotable_item_quotes_attributes: [ - :id, :quotable_type, :quotable_id, :_destroy, - quote_attributes: [ :id, :quote, :age, :workshop_id, :_destroy ] ], - all_quotable_item_quotes_attributes: [ - :id, :quotable_type, :quotable_id, :_destroy, - quote_attributes: [ :id, :quote, :age, :workshop_id, :_destroy ] ], + quotes_attributes: [ :id, :quote, :age, :speaker_name, :workshop_id, :_destroy ], report_form_field_answers_attributes: [ :id, :form_field_id, :answer_option_id, :answer, :report_id, :_destroy ], gallery_assets_attributes: [ :id, :file, :_destroy ]) diff --git a/app/decorators/quote_decorator.rb b/app/decorators/quote_decorator.rb index a7903aebb..8cce05d79 100644 --- a/app/decorators/quote_decorator.rb +++ b/app/decorators/quote_decorator.rb @@ -9,7 +9,7 @@ def detail(length: nil) end def created_by # TODO - add to model and quote creation - object.quotable_item_quotes.last&.quotable&.decorate&.created_by + object.quotable&.decorate&.created_by end def quote diff --git a/app/models/quote.rb b/app/models/quote.rb index d3601ef70..15b8f32ca 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -2,8 +2,8 @@ class Quote < ApplicationRecord include Publishable, TagFilterable, Trendable, WindowsTypeFilterable belongs_to :workshop, optional: true + belongs_to :quotable, polymorphic: true, optional: true has_many :bookmarks, as: :bookmarkable, dependent: :destroy - has_many :quotable_item_quotes, dependent: :destroy has_many :categorizable_items, dependent: :destroy, inverse_of: :categorizable, as: :categorizable has_many :sectorable_items, dependent: :destroy, inverse_of: :sectorable, as: :sectorable # Asset associations diff --git a/app/models/report.rb b/app/models/report.rb index 98e76dc25..21c07227d 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -7,7 +7,7 @@ class Report < ApplicationRecord has_one :form, as: :owner has_many :bookmarks, as: :bookmarkable, dependent: :destroy has_many :notifications, as: :noticeable, dependent: :destroy - has_many :quotable_item_quotes, as: :quotable, dependent: :nullify, inverse_of: :quotable + has_many :quotes, as: :quotable, dependent: :destroy has_many :report_form_field_answers, foreign_key: :report_id, inverse_of: :report, dependent: :destroy @@ -25,20 +25,13 @@ class Report < ApplicationRecord # has_many through has_many :form_fields, through: :form - has_many :all_quotable_item_quotes, - ->(wl) { where(quotable_id: wl.id, - quotable_type: %w[WorkshopLog Report]) }, # needed bc some are stored w type Report - class_name: "QuotableItemQuote", - inverse_of: :quotable - has_many :quotes, through: :all_quotable_item_quotes, dependent: :nullify has_many :sectors, through: :sectorable_items, dependent: :destroy # Nested attributes accepts_nested_attributes_for :media_files, allow_destroy: true, reject_if: :all_blank accepts_nested_attributes_for :primary_asset, allow_destroy: true, reject_if: :all_blank accepts_nested_attributes_for :gallery_assets, allow_destroy: true, reject_if: :all_blank - accepts_nested_attributes_for :all_quotable_item_quotes, allow_destroy: true, reject_if: :all_blank - accepts_nested_attributes_for :quotable_item_quotes, allow_destroy: true, reject_if: :all_blank + accepts_nested_attributes_for :quotes, allow_destroy: true, reject_if: :all_blank accepts_nested_attributes_for :report_form_field_answers, reject_if: proc { |object| object["_create"].to_i == 0 && object["answer"].nil? } diff --git a/app/models/workshop.rb b/app/models/workshop.rb index 106eb5975..16b7f9bb7 100644 --- a/app/models/workshop.rb +++ b/app/models/workshop.rb @@ -27,7 +27,7 @@ def self.mentionable_rich_text_fields has_many :bookmarks, as: :bookmarkable, dependent: :destroy has_many :categorizable_items, dependent: :destroy, inverse_of: :categorizable, as: :categorizable - has_many :quotable_item_quotes, as: :quotable, dependent: :destroy + has_many :quotes, as: :quotable, dependent: :destroy has_many :associated_resources, class_name: "Resource", foreign_key: "workshop_id", dependent: :restrict_with_error has_many :sectorable_items, dependent: :destroy, inverse_of: :sectorable, as: :sectorable has_many :workshop_logs, dependent: :destroy, as: :owner @@ -51,7 +51,6 @@ def self.mentionable_rich_text_fields has_many :categories, through: :categorizable_items has_many :category_types, through: :categories has_many :organizations, through: :user - has_many :quotes, through: :quotable_item_quotes has_many :resources, through: :workshop_resources, source: :resource has_many :sectors, through: :sectorable_items diff --git a/app/views/quotes/index.html.erb b/app/views/quotes/index.html.erb index 02200f6e4..9dc81bbc1 100644 --- a/app/views/quotes/index.html.erb +++ b/app/views/quotes/index.html.erb @@ -57,12 +57,10 @@ <%= "@ " + quote.created_at.strftime("%m-%d-%-Y") %> <%= ("re " + link_to(quote.workshop.title, workshop_path(quote.workshop), class: "hover:underline") if quote.workshop).to_s.html_safe %> - <% quote.quotable_item_quotes.each do |qiq| %> - <% if qiq.quotable %> - from: <%= link_to qiq.quotable.title, - polymorphic_path(qiq.quotable), - class: "hover:underline" %> - <% end %> + <% if quote.quotable %> + from: <%= link_to quote.quotable.title, + polymorphic_path(quote.quotable), + class: "hover:underline" %> <% end %> diff --git a/app/views/quotes/show.html.erb b/app/views/quotes/show.html.erb index 6d00355c4..af6badb9b 100644 --- a/app/views/quotes/show.html.erb +++ b/app/views/quotes/show.html.erb @@ -72,11 +72,11 @@ <% end %>