From 147be9571ad4a5780ea27f33f768965384a4e289 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 20 Jan 2026 00:55:06 +0000
Subject: [PATCH 01/11] Add story_populations and categorizable associations to
Story and StoryIdea models
Co-authored-by: maebeale <7607813+maebeale@users.noreply.github.com>
---
app/controllers/stories_controller.rb | 5 +-
app/controllers/story_ideas_controller.rb | 6 ++-
app/models/story.rb | 1 +
app/models/story_idea.rb | 4 ++
app/views/story_ideas/_form.html.erb | 19 +++++++
...005210_add_story_populations_to_stories.rb | 5 ++
...11_add_story_populations_to_story_ideas.rb | 5 ++
..._add_story_category_type_and_categories.rb | 49 +++++++++++++++++++
8 files changed, 92 insertions(+), 2 deletions(-)
create mode 100644 db/migrate/20260120005210_add_story_populations_to_stories.rb
create mode 100644 db/migrate/20260120005211_add_story_populations_to_story_ideas.rb
create mode 100644 db/migrate/20260120005212_add_story_category_type_and_categories.rb
diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb
index 0cdaa2dd6..deca1854a 100644
--- a/app/controllers/stories_controller.rb
+++ b/app/controllers/stories_controller.rb
@@ -163,10 +163,13 @@ def story_params
:title, :rhino_body, :featured, :published, :publicly_visible, :publicly_featured, :youtube_url, :website_url,
:windows_type_id, :organization_id, :workshop_id, :external_workshop_title,
:created_by_id, :updated_by_id, :story_idea_id, :spotlighted_facilitator_id,
+ story_populations: [],
category_ids: [],
sector_ids: [],
primary_asset_attributes: [ :id, :file, :_destroy ],
- gallery_assets_attributes: [ :id, :file, :_destroy ]
+ gallery_assets_attributes: [ :id, :file, :_destroy ],
+ categorizable_items_attributes: [ :id, :category_id, :_destroy ],
+ category_ids: []
)
end
diff --git a/app/controllers/story_ideas_controller.rb b/app/controllers/story_ideas_controller.rb
index 3e61c49e1..fa67a7167 100644
--- a/app/controllers/story_ideas_controller.rb
+++ b/app/controllers/story_ideas_controller.rb
@@ -107,8 +107,12 @@ def story_idea_params
:title, :body, :youtube_url,
:permission_given, :publish_preferences, :promoted_to_story,
:windows_type_id, :organization_id, :workshop_id, :external_workshop_title,
+ :created_by_id, :updated_by_id,
+ story_populations: [],
primary_asset_attributes: [ :id, :file, :_destroy ],
- gallery_assets_attributes: [ :id, :file, :_destroy ]
+ gallery_assets_attributes: [ :id, :file, :_destroy ],
+ categorizable_items_attributes: [ :id, :category_id, :_destroy ],
+ category_ids: []
)
end
end
diff --git a/app/models/story.rb b/app/models/story.rb
index 2b20225ee..9353fcf4e 100644
--- a/app/models/story.rb
+++ b/app/models/story.rb
@@ -37,6 +37,7 @@ class Story < ApplicationRecord
# Nested attributes
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 :categorizable_items, allow_destroy: true, reject_if: :all_blank
# SearchCop
include SearchCop
diff --git a/app/models/story_idea.rb b/app/models/story_idea.rb
index 4b57f7347..3ae5c61ec 100644
--- a/app/models/story_idea.rb
+++ b/app/models/story_idea.rb
@@ -22,6 +22,7 @@ def self.search_by_params(params)
belongs_to :windows_type
belongs_to :workshop, optional: true
has_many :bookmarks, as: :bookmarkable, dependent: :destroy
+ has_many :categorizable_items, dependent: :destroy, inverse_of: :categorizable, as: :categorizable
has_many :notifications, as: :noticeable, dependent: :destroy
has_many :stories
# Asset associations
@@ -30,6 +31,8 @@ def self.search_by_params(params)
has_many :gallery_assets, -> { where(type: "GalleryAsset") },
as: :owner, class_name: "GalleryAsset", dependent: :destroy
has_many :assets, as: :owner, dependent: :destroy
+ # has_many through
+ has_many :categories, through: :categorizable_items
# Validations
validates :created_by_id, presence: true
@@ -43,6 +46,7 @@ def self.search_by_params(params)
# Nested attributes
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 :categorizable_items, allow_destroy: true, reject_if: :all_blank
def name
"StoryIdea ##{id}"
diff --git a/app/views/story_ideas/_form.html.erb b/app/views/story_ideas/_form.html.erb
index 64dc5f9c9..1876aafd7 100644
--- a/app/views/story_ideas/_form.html.erb
+++ b/app/views/story_ideas/_form.html.erb
@@ -87,6 +87,25 @@
input_html: { rows: 10,
class: ("readonly" if promoted_to_story) } %>
+
+
+
+ Who is your story about?
+
+
+ <% ["Adults", "Children", "Colleagues", "Community", "Families", "Self", "Teens"].each do |population| %>
+
+ <%= check_box_tag "story_idea[story_populations][]", population,
+ (f.object.story_populations || []).include?(population),
+ id: "story_idea_story_populations_#{population.downcase}",
+ disabled: promoted_to_story,
+ class: "rounded border-gray-300 text-purple-600 shadow-sm focus:border-purple-300 focus:ring focus:ring-purple-200 focus:ring-opacity-50" %>
+ <%= label_tag "story_idea_story_populations_#{population.downcase}", population, class: "ml-2 text-sm text-gray-700" %>
+
+ <% end %>
+
+
+
<%= f.input :publish_preferences,
as: :select,
diff --git a/db/migrate/20260120005210_add_story_populations_to_stories.rb b/db/migrate/20260120005210_add_story_populations_to_stories.rb
new file mode 100644
index 000000000..466e52c3d
--- /dev/null
+++ b/db/migrate/20260120005210_add_story_populations_to_stories.rb
@@ -0,0 +1,5 @@
+class AddStoryPopulationsToStories < ActiveRecord::Migration[8.1]
+ def change
+ add_column :stories, :story_populations, :json
+ end
+end
diff --git a/db/migrate/20260120005211_add_story_populations_to_story_ideas.rb b/db/migrate/20260120005211_add_story_populations_to_story_ideas.rb
new file mode 100644
index 000000000..5d0c0a75b
--- /dev/null
+++ b/db/migrate/20260120005211_add_story_populations_to_story_ideas.rb
@@ -0,0 +1,5 @@
+class AddStoryPopulationsToStoryIdeas < ActiveRecord::Migration[8.1]
+ def change
+ add_column :story_ideas, :story_populations, :json
+ end
+end
diff --git a/db/migrate/20260120005212_add_story_category_type_and_categories.rb b/db/migrate/20260120005212_add_story_category_type_and_categories.rb
new file mode 100644
index 000000000..ac5648f8a
--- /dev/null
+++ b/db/migrate/20260120005212_add_story_category_type_and_categories.rb
@@ -0,0 +1,49 @@
+class AddStoryCategoryTypeAndCategories < ActiveRecord::Migration[8.1]
+ def up
+ # Create the StoryCategory type if it doesn't exist
+ story_category_type = CategoryType.find_or_create_by!(name: "StoryCategory")
+
+ # List of story categories from the WordPress form
+ story_categories = [
+ "Advocacy (legislation, voter mobilization, etc.)",
+ "Batterers Intervention (working with people who have caused harm)",
+ "Child Abuse / Neglect",
+ "Community Engagement",
+ "Community Violence (gang violence, police violence, mass shootings etc.)",
+ "Court & Legal System (including law enforcement and probation)",
+ "Disability Services",
+ "Domestic Violence",
+ "Donor Engagement",
+ "Foster Care",
+ "Grief & Loss",
+ "Homelessness",
+ "Human Trafficking",
+ "Immigration (family separation, deportation, refugees/asylees, etc.)",
+ "Incarceration",
+ "LGBTQIA+",
+ "Mental Health (DSM, suicidal ideation, etc.)",
+ "Military & Veterans",
+ "Physical & Terminal Illness",
+ "Schools & Universities",
+ "Self-Care & Personal Growth",
+ "Sexual Assault",
+ "Social Justice (anti-oppression, restorative justice, systems change, etc.)",
+ "Staff & Organizational Development",
+ "Substance Abuse Recovery"
+ ]
+
+ # Create each category with proper ordering
+ story_categories.each_with_index do |category_name, index|
+ story_category_type.categories.find_or_create_by!(name: category_name) do |category|
+ category.position = (index + 1) * 10
+ category.published = true
+ end
+ end
+ end
+
+ def down
+ story_category_type = CategoryType.find_by(name: "StoryCategory")
+ story_category_type&.categories&.destroy_all
+ story_category_type&.destroy
+ end
+end
From 6a51ad0403e592239ca4c0574c3148548c6f9bdc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 20 Jan 2026 00:57:30 +0000
Subject: [PATCH 02/11] Add story_categories scope to Category model and update
controllers
Co-authored-by: maebeale <7607813+maebeale@users.noreply.github.com>
---
app/models/category.rb | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/models/category.rb b/app/models/category.rb
index 811f620e5..80457c213 100644
--- a/app/models/category.rb
+++ b/app/models/category.rb
@@ -10,6 +10,7 @@ class Category < ApplicationRecord
# Scopes
# See NameFilterable, Publishable
scope :age_ranges, -> { joins(:category_type).where(category_types: { name: "AgeRange" }) }
+ scope :story_categories, -> { joins(:category_type).where(category_types: { name: "StoryCategory" }) }
scope :ordered_by_position_and_name, -> { reorder(position: :asc, name: :asc) }
# Validations
From df2e1ce310b4d41318ccb2104b837bf861ecef57 Mon Sep 17 00:00:00 2001
From: maebeale
Date: Sun, 15 Feb 2026 09:48:37 -0500
Subject: [PATCH 03/11] WIP: story updates
---
app/controllers/stories_controller.rb | 1 -
app/controllers/story_ideas_controller.rb | 1 -
app/views/story_ideas/_form.html.erb | 24 ++-------
...005210_add_story_populations_to_stories.rb | 5 --
...11_add_story_populations_to_story_ideas.rb | 5 --
..._add_story_category_type_and_categories.rb | 49 -------------------
6 files changed, 4 insertions(+), 81 deletions(-)
delete mode 100644 db/migrate/20260120005210_add_story_populations_to_stories.rb
delete mode 100644 db/migrate/20260120005211_add_story_populations_to_story_ideas.rb
delete mode 100644 db/migrate/20260120005212_add_story_category_type_and_categories.rb
diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb
index deca1854a..dc9db5239 100644
--- a/app/controllers/stories_controller.rb
+++ b/app/controllers/stories_controller.rb
@@ -163,7 +163,6 @@ def story_params
:title, :rhino_body, :featured, :published, :publicly_visible, :publicly_featured, :youtube_url, :website_url,
:windows_type_id, :organization_id, :workshop_id, :external_workshop_title,
:created_by_id, :updated_by_id, :story_idea_id, :spotlighted_facilitator_id,
- story_populations: [],
category_ids: [],
sector_ids: [],
primary_asset_attributes: [ :id, :file, :_destroy ],
diff --git a/app/controllers/story_ideas_controller.rb b/app/controllers/story_ideas_controller.rb
index fa67a7167..de7766966 100644
--- a/app/controllers/story_ideas_controller.rb
+++ b/app/controllers/story_ideas_controller.rb
@@ -108,7 +108,6 @@ def story_idea_params
:permission_given, :publish_preferences, :promoted_to_story,
:windows_type_id, :organization_id, :workshop_id, :external_workshop_title,
:created_by_id, :updated_by_id,
- story_populations: [],
primary_asset_attributes: [ :id, :file, :_destroy ],
gallery_assets_attributes: [ :id, :file, :_destroy ],
categorizable_items_attributes: [ :id, :category_id, :_destroy ],
diff --git a/app/views/story_ideas/_form.html.erb b/app/views/story_ideas/_form.html.erb
index 1876aafd7..8362a0c5b 100644
--- a/app/views/story_ideas/_form.html.erb
+++ b/app/views/story_ideas/_form.html.erb
@@ -81,31 +81,15 @@
<%= f.input :body,
as: :text,
- label: "Share details of your story",
- hint: "(HTML allowed)",
+ label: "Please share details about your participant(s) (while maintaining confidentiality)
+ and their workshop goals, including what happened during the workshop, and how creating made an impact.
+ Include direct quotes whenever possible.",
+ hint: "(For more tips on what makes a powerful story, see the story tips PDF above.)",
disabled: promoted_to_story,
input_html: { rows: 10,
class: ("readonly" if promoted_to_story) } %>
-
-
- Who is your story about?
-
-
- <% ["Adults", "Children", "Colleagues", "Community", "Families", "Self", "Teens"].each do |population| %>
-
- <%= check_box_tag "story_idea[story_populations][]", population,
- (f.object.story_populations || []).include?(population),
- id: "story_idea_story_populations_#{population.downcase}",
- disabled: promoted_to_story,
- class: "rounded border-gray-300 text-purple-600 shadow-sm focus:border-purple-300 focus:ring focus:ring-purple-200 focus:ring-opacity-50" %>
- <%= label_tag "story_idea_story_populations_#{population.downcase}", population, class: "ml-2 text-sm text-gray-700" %>
-
- <% end %>
-
-
-
<%= f.input :publish_preferences,
as: :select,
diff --git a/db/migrate/20260120005210_add_story_populations_to_stories.rb b/db/migrate/20260120005210_add_story_populations_to_stories.rb
deleted file mode 100644
index 466e52c3d..000000000
--- a/db/migrate/20260120005210_add_story_populations_to_stories.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddStoryPopulationsToStories < ActiveRecord::Migration[8.1]
- def change
- add_column :stories, :story_populations, :json
- end
-end
diff --git a/db/migrate/20260120005211_add_story_populations_to_story_ideas.rb b/db/migrate/20260120005211_add_story_populations_to_story_ideas.rb
deleted file mode 100644
index 5d0c0a75b..000000000
--- a/db/migrate/20260120005211_add_story_populations_to_story_ideas.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class AddStoryPopulationsToStoryIdeas < ActiveRecord::Migration[8.1]
- def change
- add_column :story_ideas, :story_populations, :json
- end
-end
diff --git a/db/migrate/20260120005212_add_story_category_type_and_categories.rb b/db/migrate/20260120005212_add_story_category_type_and_categories.rb
deleted file mode 100644
index ac5648f8a..000000000
--- a/db/migrate/20260120005212_add_story_category_type_and_categories.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-class AddStoryCategoryTypeAndCategories < ActiveRecord::Migration[8.1]
- def up
- # Create the StoryCategory type if it doesn't exist
- story_category_type = CategoryType.find_or_create_by!(name: "StoryCategory")
-
- # List of story categories from the WordPress form
- story_categories = [
- "Advocacy (legislation, voter mobilization, etc.)",
- "Batterers Intervention (working with people who have caused harm)",
- "Child Abuse / Neglect",
- "Community Engagement",
- "Community Violence (gang violence, police violence, mass shootings etc.)",
- "Court & Legal System (including law enforcement and probation)",
- "Disability Services",
- "Domestic Violence",
- "Donor Engagement",
- "Foster Care",
- "Grief & Loss",
- "Homelessness",
- "Human Trafficking",
- "Immigration (family separation, deportation, refugees/asylees, etc.)",
- "Incarceration",
- "LGBTQIA+",
- "Mental Health (DSM, suicidal ideation, etc.)",
- "Military & Veterans",
- "Physical & Terminal Illness",
- "Schools & Universities",
- "Self-Care & Personal Growth",
- "Sexual Assault",
- "Social Justice (anti-oppression, restorative justice, systems change, etc.)",
- "Staff & Organizational Development",
- "Substance Abuse Recovery"
- ]
-
- # Create each category with proper ordering
- story_categories.each_with_index do |category_name, index|
- story_category_type.categories.find_or_create_by!(name: category_name) do |category|
- category.position = (index + 1) * 10
- category.published = true
- end
- end
- end
-
- def down
- story_category_type = CategoryType.find_by(name: "StoryCategory")
- story_category_type&.categories&.destroy_all
- story_category_type&.destroy
- end
-end
From e77035fb63c975a07df717b5a879146898203cd4 Mon Sep 17 00:00:00 2001
From: maebeale
Date: Sun, 15 Feb 2026 11:57:30 -0500
Subject: [PATCH 04/11] Add story_specific as category_type boolean and use it
only on stories and not other places tags show
---
app/controllers/community_news_controller.rb | 2 +-
app/controllers/events_controller.rb | 2 +-
app/controllers/resources_controller.rb | 2 +-
app/controllers/stories_controller.rb | 2 +-
app/controllers/workshop_ideas_controller.rb | 2 +-
app/controllers/workshops_controller.rb | 2 +-
app/models/sector.rb | 2 ++
...add_story_specific_and_display_text_to_category_types.rb | 6 ++++++
8 files changed, 14 insertions(+), 6 deletions(-)
create mode 100644 db/migrate/20260215000000_add_story_specific_and_display_text_to_category_types.rb
diff --git a/app/controllers/community_news_controller.rb b/app/controllers/community_news_controller.rb
index e9ea567b0..3b0a922a4 100644
--- a/app/controllers/community_news_controller.rb
+++ b/app/controllers/community_news_controller.rb
@@ -117,7 +117,7 @@ def set_form_variables
.published
.order(:position, :name)
.group_by(&:category_type)
- .select { |type, _| type.nil? || type.published? }
+ .select { |type, _| type.nil? || (type.published? && !type.story_specific?) }
.sort_by { |type, _| type&.name.to_s.downcase }
@sectors = Sector.published.order(:name)
@community_news.build_primary_asset if @community_news.primary_asset.blank?
diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb
index f46054aaf..2ac026004 100644
--- a/app/controllers/events_controller.rb
+++ b/app/controllers/events_controller.rb
@@ -107,7 +107,7 @@ def set_form_variables
.published
.order(:position, :name)
.group_by(&:category_type)
- .select { |type, _| type.nil? || type.published? }
+ .select { |type, _| type.nil? || (type.published? && !type.story_specific?) }
.sort_by { |type, _| type&.name.to_s.downcase }
@sectors = Sector.published.order(:name)
end
diff --git a/app/controllers/resources_controller.rb b/app/controllers/resources_controller.rb
index 8c9da4b8f..2b42dfce5 100644
--- a/app/controllers/resources_controller.rb
+++ b/app/controllers/resources_controller.rb
@@ -159,7 +159,7 @@ def set_form_variables
.published
.order(:position, :name)
.group_by(&:category_type)
- .select { |type, _| type.nil? || type.published? }
+ .select { |type, _| type.nil? || (type.published? && !type.story_specific?) }
.sort_by { |type, _| type&.name.to_s.downcase }
@sectors = Sector.published.order(:name)
end
diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb
index dc9db5239..aead5b073 100644
--- a/app/controllers/stories_controller.rb
+++ b/app/controllers/stories_controller.rb
@@ -135,7 +135,7 @@ def set_form_variables
.order(:position, :name)
.group_by(&:category_type)
.select { |type, _| type.nil? || type.published? }
- .sort_by { |type, _| type&.name.to_s.downcase }
+ .sort_by { |type, _| [type&.story_specific? ? 0 : 1, type&.name.to_s.downcase] }
@sectors = Sector.published.order(:name)
@story.build_primary_asset if @story.primary_asset.blank?
@story.gallery_assets.build
diff --git a/app/controllers/workshop_ideas_controller.rb b/app/controllers/workshop_ideas_controller.rb
index 6d4435c86..86f8de0ae 100644
--- a/app/controllers/workshop_ideas_controller.rb
+++ b/app/controllers/workshop_ideas_controller.rb
@@ -79,7 +79,7 @@ def set_form_variables
.published
.order(:position, :name)
.group_by(&:category_type)
- .select { |type, _| type.nil? || type.published? }
+ .select { |type, _| type.nil? || (type.published? && !type.story_specific?) }
.sort_by { |type, _| type&.name.to_s.downcase }
@workshop_idea.build_primary_asset if @workshop_idea.primary_asset.blank?
@workshop_idea.gallery_assets.build
diff --git a/app/controllers/workshops_controller.rb b/app/controllers/workshops_controller.rb
index 01fbcf61a..25efd5af9 100644
--- a/app/controllers/workshops_controller.rb
+++ b/app/controllers/workshops_controller.rb
@@ -201,7 +201,7 @@ def set_form_variables
.published
.order(:position, :name)
.group_by(&:category_type)
- .select { |type, _| type.nil? || type.published? }
+ .select { |type, _| type.nil? || (type.published? && !type.story_specific?) }
.sort_by { |type, _| type&.name.to_s.downcase }
@sectors = Sector.published.order(:name)
diff --git a/app/models/sector.rb b/app/models/sector.rb
index 968042fee..e1323d903 100644
--- a/app/models/sector.rb
+++ b/app/models/sector.rb
@@ -24,6 +24,8 @@ class Sector < ApplicationRecord
"Other"
]
+ STORY_DISPLAY_TEXT = "Which sectors apply?"
+
has_many :sectorable_items, dependent: :destroy
has_many :workshops, through: :sectorable_items,
source: :sectorable, source_type: "Workshop"
diff --git a/db/migrate/20260215000000_add_story_specific_and_display_text_to_category_types.rb b/db/migrate/20260215000000_add_story_specific_and_display_text_to_category_types.rb
new file mode 100644
index 000000000..b515a2a4c
--- /dev/null
+++ b/db/migrate/20260215000000_add_story_specific_and_display_text_to_category_types.rb
@@ -0,0 +1,6 @@
+class AddStorySpecificAndDisplayTextToCategoryTypes < ActiveRecord::Migration[8.1]
+ def change
+ add_column :category_types, :story_specific, :boolean, default: false
+ add_column :category_types, :display_text, :string
+ end
+end
From bab3886a06c3dd6aa7217f49426fecacf15914c8 Mon Sep 17 00:00:00 2001
From: maebeale
Date: Sun, 15 Feb 2026 11:59:22 -0500
Subject: [PATCH 05/11] Story and StoryIdea display and promotion process wip
---
app/controllers/stories_controller.rb | 19 ++-
app/controllers/story_ideas_controller.rb | 84 ++++++++--
app/helpers/title_display_helper.rb | 10 ++
app/models/category_type.rb | 6 +
app/models/story.rb | 2 +-
app/models/story_idea.rb | 3 +-
app/views/stories/_form.html.erb | 103 ++++++++----
app/views/story_ideas/_form.html.erb | 193 +++++++++++++++++++---
app/views/story_ideas/edit.html.erb | 5 -
9 files changed, 346 insertions(+), 79 deletions(-)
diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb
index aead5b073..09cad9a4d 100644
--- a/app/controllers/stories_controller.rb
+++ b/app/controllers/stories_controller.rb
@@ -57,7 +57,7 @@ def edit
end
def create
- @story = Story.new(story_params)
+ @story = Story.new(story_params.except(:category_ids, :sector_ids))
authorize! @story
success = false
@@ -91,8 +91,11 @@ def update
success = false
Story.transaction do
- if @story.update(story_params.except(:images))
+ if @story.update(story_params.except(:images, :category_ids, :sector_ids))
assign_associations(@story)
+ if params[:promote_idea_assets] == "true"
+ @story.attach_assets_from_idea!
+ end
success = true
end
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
@@ -137,6 +140,15 @@ def set_form_variables
.select { |type, _| type.nil? || type.published? }
.sort_by { |type, _| [type&.story_specific? ? 0 : 1, type&.name.to_s.downcase] }
@sectors = Sector.published.order(:name)
+ submitted_sector_ids = Array(params.dig(:story, :sector_ids)).reject(&:blank?)
+ submitted_category_ids = Array(params.dig(:story, :category_ids)).reject(&:blank?)
+ if submitted_sector_ids.any? || submitted_category_ids.any?
+ @preselected_sector_ids = submitted_sector_ids.map(&:to_i)
+ @preselected_category_ids = submitted_category_ids.map(&:to_i)
+ elsif @story_idea
+ @preselected_sector_ids = @story_idea.sector_ids
+ @preselected_category_ids = @story_idea.category_ids
+ end
@story.build_primary_asset if @story.primary_asset.blank?
@story.gallery_assets.build
end
@@ -167,13 +179,12 @@ def story_params
sector_ids: [],
primary_asset_attributes: [ :id, :file, :_destroy ],
gallery_assets_attributes: [ :id, :file, :_destroy ],
- categorizable_items_attributes: [ :id, :category_id, :_destroy ],
- category_ids: []
)
end
def set_story_attributes_from(idea)
{
+ story_idea_id: idea.id,
rhino_body: idea.body,
organization_id: idea.organization.id,
workshop_id: idea.workshop_id,
diff --git a/app/controllers/story_ideas_controller.rb b/app/controllers/story_ideas_controller.rb
index de7766966..cbbd1cc7b 100644
--- a/app/controllers/story_ideas_controller.rb
+++ b/app/controllers/story_ideas_controller.rb
@@ -31,19 +31,30 @@ def edit
end
def create
- @story_idea = StoryIdea.new(story_idea_params)
+ @story_idea = StoryIdea.new(story_idea_params.except(:category_ids, :sector_ids))
@story_idea.created_by = current_user
@story_idea.updated_by = current_user
authorize! @story_idea
- if @story_idea.save
- NotificationServices::CreateNotification.call(
- noticeable: @story_idea,
- kind: :idea_submitted_fyi,
- recipient_role: :admin,
- recipient_email: ENV.fetch("REPLY_TO_EMAIL", "programs@awbw.org"),
- notification_type: 0)
+ success = false
+
+ StoryIdea.transaction do
+ if @story_idea.save
+ assign_associations(@story_idea)
+ NotificationServices::CreateNotification.call(
+ noticeable: @story_idea,
+ kind: :idea_submitted_fyi,
+ recipient_role: :admin,
+ recipient_email: ENV.fetch("REPLY_TO_EMAIL", "programs@awbw.org"),
+ notification_type: 0)
+ success = true
+ end
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
+ Rails.logger.error "StoryIdea create failed: #{e.class} - #{e.message}"
+ raise ActiveRecord::Rollback
+ end
+ if success
flash[:notice] = "StoryIdea was successfully created."
if allowed_to?(:index?, StoryIdea)
redirect_to story_ideas_path
@@ -60,7 +71,19 @@ def update
@story_idea.updated_by = current_user
authorize! @story_idea
- if @story_idea.update(story_idea_params.except(:images))
+ success = false
+
+ StoryIdea.transaction do
+ if @story_idea.update(story_idea_params.except(:images, :category_ids, :sector_ids))
+ assign_associations(@story_idea)
+ success = true
+ end
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
+ Rails.logger.error "StoryIdea update failed: #{e.class} - #{e.message}"
+ raise ActiveRecord::Rollback
+ end
+
+ if success
flash[:notice] = "StoryIdea was successfully updated."
if allowed_to?(:index?, StoryIdea)
redirect_to story_ideas_path, status: :see_other
@@ -89,13 +112,44 @@ def set_form_variables
@workshops = authorized_scope(Workshop.all).includes(:windows_type).order(:title)
- @users = authorized_scope(User.has_access.includes(:person))
- @users = @users.or(User.where(id: @story_idea.created_by_id)) if @story_idea&.created_by_id
- @users = @users.includes(:person).distinct.order("people.first_name, people.last_name")
+ users = authorized_scope(User.has_access.includes(:person))
+ users = users.or(User.where(id: @story_idea.created_by_id)) if @story_idea&.created_by_id
+ @users = users.distinct.order("people.first_name, people.last_name")
+
+ @story_population_type = CategoryType.find_by(name: "StoryPopulation")
+ @story_population_categories = @story_population_type&.categories&.published&.order(:name) || []
+ @sectors = Sector.published.order(:name)
+ submitted_sector_ids = Array(params.dig(:story_idea, :sector_ids)).reject(&:blank?)
+ submitted_category_ids = Array(params.dig(:story_idea, :category_ids)).reject(&:blank?)
+ if submitted_sector_ids.any? || submitted_category_ids.any?
+ @preselected_sector_ids = submitted_sector_ids.map(&:to_i)
+ @preselected_category_ids = submitted_category_ids.map(&:to_i)
+ end
+
+ if @story_idea.persisted?
+ @categories_grouped =
+ Category
+ .includes(:category_type)
+ .published
+ .order(:position, :name)
+ .group_by(&:category_type)
+ .select { |type, _| type.nil? || type.published? }
+ .sort_by { |type, _| [type&.story_specific? ? 0 : 1, type&.name.to_s.downcase] }
+ end
+>>>>>>> ac3201a15 (Story and StoryIdea display and promotion process wip)
@story_idea.build_primary_asset if @story_idea.primary_asset.blank?
@story_idea.gallery_assets.build
end
+ def assign_associations(story_idea)
+ selected_category_ids = Array(params[:story_idea][:category_ids]).reject(&:blank?).map(&:to_i)
+ story_idea.categories = Category.where(id: selected_category_ids)
+
+ selected_sector_ids = Array(params[:story_idea][:sector_ids]).reject(&:blank?).map(&:to_i)
+ story_idea.sectors = Sector.where(id: selected_sector_ids)
+ story_idea.save!
+ end
+
private
def set_story_idea
@@ -108,10 +162,10 @@ def story_idea_params
:permission_given, :publish_preferences, :promoted_to_story,
:windows_type_id, :organization_id, :workshop_id, :external_workshop_title,
:created_by_id, :updated_by_id,
+ category_ids: [],
+ sector_ids: [],
primary_asset_attributes: [ :id, :file, :_destroy ],
- gallery_assets_attributes: [ :id, :file, :_destroy ],
- categorizable_items_attributes: [ :id, :category_id, :_destroy ],
- category_ids: []
+ gallery_assets_attributes: [ :id, :file, :_destroy ]
)
end
end
diff --git a/app/helpers/title_display_helper.rb b/app/helpers/title_display_helper.rb
index 21249f4db..a454844db 100644
--- a/app/helpers/title_display_helper.rb
+++ b/app/helpers/title_display_helper.rb
@@ -23,6 +23,16 @@ def title_with_badges(record, font_size: "text-lg", record_title: nil,
)
end
+ # --- Promoted from story idea badge ---
+ if record.respond_to?(:story_idea) && record.story_idea.present?
+ fragments << content_tag(
+ :span,
+ content_tag(:i, "", class: "fa-solid fa-arrow-up-from-bracket mr-1") + " Promoted",
+ class: "inline-flex items-center px-2 py-0.5 rounded-full
+ text-sm font-medium bg-green-100 text-green-800 whitespace-nowrap"
+ )
+ end
+
title_content = record_title || record.title.to_s
if display_windows_type && record.respond_to?(:windows_type) && record.windows_type.present?
diff --git a/app/models/category_type.rb b/app/models/category_type.rb
index 4a8abd1c9..0952f338d 100644
--- a/app/models/category_type.rb
+++ b/app/models/category_type.rb
@@ -9,4 +9,10 @@ class CategoryType < ApplicationRecord
# Scopes
# See Publishable
+ scope :general, -> { where(story_specific: false) }
+ scope :story_specific, -> { where(story_specific: true) }
+
+ def display_label
+ display_text.presence || name.titleize
+ end
end
diff --git a/app/models/story.rb b/app/models/story.rb
index 9353fcf4e..5dffdf2c8 100644
--- a/app/models/story.rb
+++ b/app/models/story.rb
@@ -37,7 +37,6 @@ class Story < ApplicationRecord
# Nested attributes
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 :categorizable_items, allow_destroy: true, reject_if: :all_blank
# SearchCop
include SearchCop
@@ -94,6 +93,7 @@ def sector_names_all
def attach_assets_from_idea!
return unless story_idea
+ assets.destroy_all
story_idea.assets.find_each do |asset|
new_asset = assets.build(type: asset.type)
new_asset.file.attach(asset.file.blob)
diff --git a/app/models/story_idea.rb b/app/models/story_idea.rb
index 3ae5c61ec..c0124a2be 100644
--- a/app/models/story_idea.rb
+++ b/app/models/story_idea.rb
@@ -23,6 +23,7 @@ def self.search_by_params(params)
belongs_to :workshop, optional: true
has_many :bookmarks, as: :bookmarkable, dependent: :destroy
has_many :categorizable_items, dependent: :destroy, inverse_of: :categorizable, as: :categorizable
+ has_many :sectorable_items, dependent: :destroy, inverse_of: :sectorable, as: :sectorable
has_many :notifications, as: :noticeable, dependent: :destroy
has_many :stories
# Asset associations
@@ -33,6 +34,7 @@ def self.search_by_params(params)
has_many :assets, as: :owner, dependent: :destroy
# has_many through
has_many :categories, through: :categorizable_items
+ has_many :sectors, through: :sectorable_items
# Validations
validates :created_by_id, presence: true
@@ -46,7 +48,6 @@ def self.search_by_params(params)
# Nested attributes
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 :categorizable_items, allow_destroy: true, reject_if: :all_blank
def name
"StoryIdea ##{id}"
diff --git a/app/views/stories/_form.html.erb b/app/views/stories/_form.html.erb
index fbb6424d5..a74bb577f 100644
--- a/app/views/stories/_form.html.erb
+++ b/app/views/stories/_form.html.erb
@@ -2,6 +2,18 @@
<%= render 'shared/errors', resource: @story if @story.errors.any? %>
<% story_idea = @story_idea || f.object.story_idea %>
+ <% selected_sector_ids = @preselected_sector_ids || @story.sector_ids %>
+ <% selected_category_ids = @preselected_category_ids || @story.category_ids %>
+
+ <% if story_idea %>
+
+
+
+ Promoted from
+ <%= link_to story_idea.full_name, edit_story_idea_path(story_idea), class: "underline hover:text-blue-600" %>
+
+
+ <% end %>
<%= f.input :title, as: :text,
@@ -68,18 +80,23 @@
+ <% story_idea_label = if f.object.story_idea
+ (link_to "Source story idea",
+ story_idea_path(f.object.story_idea), class: "hover:underline")
+ else
+ "Source story idea"
+ end %>
<%= f.input :story_idea_id,
collection: @story_ideas, label_method: :full_name,
value_method: :id,
selected: f.object.story_idea_id || @story_idea&.id,
- label: (f.object.story_idea ? (link_to "Story idea",
- story_idea_path(story_idea), class: "hover:underline") : "Story idea").html_safe,
+ label: story_idea_label.html_safe,
prompt: "Select idea" %>
<%= f.input :created_by_id,
- collection: @users, label_method: :full_name,
+ collection: @users, label_method: :full_name_with_email,
value_method: :id,
selected: f.object.organization_id || @story_idea&.organization_id,
label: (f.object.created_by ? (
@@ -92,12 +109,15 @@
Story idea author credit:
- <%= story_idea.author_credit %>
-
+ <% if story_idea.created_by.person %>
+ <%= person_profile_button(story_idea.created_by.person, display_name: story_idea.author_credit, subtitle: story_idea.created_by.person.full_name) %>
+ <% else %>
+ <%= story_idea.author_credit %>
+ <% end %>
Story idea "publish preferences":
-
<%= story_idea.publish_preferences %>
+
<%= link_to story_idea.publish_preferences, edit_story_idea_path(story_idea, anchor: "publish-preferences"), class: "hover:underline hover:text-blue-600" %>
<% end %>
@@ -126,7 +146,45 @@
<% end %>
- <%= render "shared/form_image_fields", f: f, include_primary_asset: true %>
+ <% if story_idea %>
+ <% idea_assets = story_idea.assets.select { |a| a.file.attached? } %>
+ <% if idea_assets.any? %>
+ <% idea_blob_ids = idea_assets.map { |a| a.file.blob_id }.sort %>
+ <% story_blob_ids = f.object.assets.select { |a| a.file.attached? }.map { |a| a.file.blob_id }.sort %>
+ <% assets_already_match = idea_blob_ids == story_blob_ids %>
+ <% unless f.object.persisted? && assets_already_match %>
+
+
+ <%= label_tag :promote_idea_assets, class: "flex items-start space-x-3 cursor-pointer" do %>
+ <%= check_box_tag :promote_idea_assets, true, f.object.new_record?, class: "mt-1 h-5 w-5 text-blue-600 rounded border-gray-300 focus:ring-blue-500" %>
+
+
+ Check this box to transfer attachments from the story idea.
+
+
+
+ This will replace any existing attachments on this story once you click submit.
+
+
+ <% end %>
+
+
+
<%= link_to "Story idea attachments:", edit_story_idea_path(story_idea, anchor: "attachments"), class: "underline hover:text-blue-600" %>
+
+ <% idea_assets.each do |asset| %>
+
+ <%= render "assets/display_image", resource: story_idea, item: asset, variant: :gallery %>
+ <%= asset.type.underscore.humanize %>
+
+ <% end %>
+
+
+ <% end %>
+ <% end %>
+ <% end %>
+
+ <%= render "shared/form_image_fields", f: f, include_primary_asset: true,
+ primary_title: "Primary attachment", gallery_title: "Additional attachment" %>
@@ -136,13 +194,13 @@
id="tags_button"
class="
flex items-center justify-between cursor-pointer w-full
- bg-gray-500 text-white hover:bg-gray-300 px-3 py-2 rounded-md"
+ bg-gray-500 text-white hover:bg-gray-300 px-3 py-2 rounded-t-md"
data-action="dropdown#toggle"
data-dropdown-payload-param='[{"tags":"hidden"}, {"tags_arrow":"rotate-180"}, {"tags_button":"rounded-md rounded-t-md"}]'>
Tags
@@ -154,11 +212,11 @@
-