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 0cdaa2dd6..4b915ff98 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 @@ -135,8 +138,17 @@ 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) + 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 @@ -166,12 +178,13 @@ def story_params category_ids: [], sector_ids: [], primary_asset_attributes: [ :id, :file, :_destroy ], - gallery_assets_attributes: [ :id, :file, :_destroy ] + gallery_assets_attributes: [ :id, :file, :_destroy ], ) 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 3e61c49e1..a99572a7c 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,43 @@ 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 @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 @@ -107,6 +160,9 @@ 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, + category_ids: [], + sector_ids: [], primary_asset_attributes: [ :id, :file, :_destroy ], gallery_assets_attributes: [ :id, :file, :_destroy ] ) 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/helpers/person_helper.rb b/app/helpers/person_helper.rb index 189ad27be..6d997f199 100644 --- a/app/helpers/person_helper.rb +++ b/app/helpers/person_helper.rb @@ -1,5 +1,5 @@ module PersonHelper - def person_profile_button(person, truncate_at: nil, subtitle: nil) + def person_profile_button(person, truncate_at: nil, subtitle: nil, display_name: nil) bg = DomainTheme.bg_class_for(:people, intensity: 100) hover_bg = DomainTheme.bg_class_for(:people, intensity: 100, hover: true) text = DomainTheme.text_class_for(:people) @@ -28,7 +28,8 @@ def person_profile_button(person, truncate_at: nil, subtitle: nil) border border-sky-300 shadow-sm flex-shrink-0") end - display_name = truncate_at ? truncate(person.name.to_s, length: truncate_at) : person.name.to_s + display_name = display_name || person.name.to_s + display_name = truncate(display_name, length: truncate_at) if truncate_at name = content_tag( :span, 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.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 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/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/app/models/story.rb b/app/models/story.rb index 2b20225ee..5dffdf2c8 100644 --- a/app/models/story.rb +++ b/app/models/story.rb @@ -93,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 4b57f7347..c0124a2be 100644 --- a/app/models/story_idea.rb +++ b/app/models/story_idea.rb @@ -22,6 +22,8 @@ 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 :sectorable_items, dependent: :destroy, inverse_of: :sectorable, as: :sectorable has_many :notifications, as: :noticeable, dependent: :destroy has_many :stories # Asset associations @@ -30,6 +32,9 @@ 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 + has_many :sectors, through: :sectorable_items # Validations validates :created_by_id, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index adc9f1f6e..d1fd26642 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -115,6 +115,10 @@ def full_name end end + def full_name_with_email + "#{full_name} (#{email})" + end + def devise_email_name person&.first_name.presence || first_name.presence || email end 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 @@ -