diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb
index 0e6b8007e..1daed96ba 100644
--- a/app/controllers/stories_controller.rb
+++ b/app/controllers/stories_controller.rb
@@ -28,7 +28,12 @@ def show
end
def new
- @story = Story.new.decorate
+ if params[:story_idea_id].present?
+ @story_idea = StoryIdea.find(params[:story_idea_id])
+ @story = StoryFromIdeaService.new(@story_idea, user: current_user).call
+ else
+ @story = Story.new
+ end
@story = @story.decorate
set_form_variables
end
diff --git a/app/services/story_from_idea_service.rb b/app/services/story_from_idea_service.rb
new file mode 100644
index 000000000..8b1dea621
--- /dev/null
+++ b/app/services/story_from_idea_service.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+class StoryFromIdeaService
+ def initialize(story_idea, user:)
+ @story_idea = story_idea
+ @user = user
+ end
+
+ def call
+ Story.new(attributes_from_idea).tap do |story|
+ duplicate_assets(story)
+ end
+ end
+
+ private
+
+ attr_reader :story_idea, :user
+
+ def attributes_from_idea
+ story_idea.attributes.slice(
+ "title", "body", "youtube_url",
+ "windows_type_id", "project_id", "workshop_id", "external_workshop_title"
+ ).merge(
+ created_by_id: user.id,
+ updated_by_id: user.id,
+ story_idea_id: story_idea.id,
+ published: false
+ )
+ end
+
+ def duplicate_assets(story)
+ # Duplicate primary asset with correct type
+ if story_idea.primary_asset&.file&.attached?
+ story.build_primary_asset(file: story_idea.primary_asset.file.blob)
+ end
+
+ # Duplicate gallery assets with correct type
+ story_idea.gallery_assets.each do |gallery_asset|
+ next unless gallery_asset.file&.attached?
+
+ story.gallery_assets.build(file: gallery_asset.file.blob)
+ end
+ end
+end
diff --git a/app/services/workshop_from_idea_service.rb b/app/services/workshop_from_idea_service.rb
index 04d68b1c4..5e89337fa 100644
--- a/app/services/workshop_from_idea_service.rb
+++ b/app/services/workshop_from_idea_service.rb
@@ -63,8 +63,16 @@ def duplicate_series_children(workshop)
end
def duplicate_assets(workshop)
- workshop_idea.assets.each do |image|
- workshop.assets.build(file: image.file.blob)
+ # Duplicate primary asset with correct type
+ if workshop_idea.primary_asset&.file&.attached?
+ workshop.build_primary_asset(file: workshop_idea.primary_asset.file.blob)
+ end
+
+ # Duplicate gallery assets with correct type
+ workshop_idea.gallery_assets.each do |gallery_asset|
+ next unless gallery_asset.file&.attached?
+
+ workshop.gallery_assets.build(file: gallery_asset.file.blob)
end
end
end
diff --git a/app/views/workshop_ideas/edit.html.erb b/app/views/workshop_ideas/edit.html.erb
index c4e5d622d..525762989 100644
--- a/app/views/workshop_ideas/edit.html.erb
+++ b/app/views/workshop_ideas/edit.html.erb
@@ -3,8 +3,21 @@
Edit <%= @workshop_idea.class.model_name.human %>
- <%= link_to 'View', workshop_idea_path(@workshop_idea),
- class: "btn btn-secondary-outline" %>
+
+ <% if @workshop_idea.workshops.any? %>
+ <% @workshop_idea.workshops.each do |workshop| %>
+ <%= link_to "View Workshop", workshop_path(workshop),
+ class: "btn btn-secondary-outline" %>
+ <% end %>
+ <% else %>
+ <% if current_user.super_user && @workshop_idea.persisted? %>
+ <%= link_to "Promote to Workshop", new_workshop_path(workshop_idea_id: @workshop_idea.id),
+ class: "admin-only bg-blue-100 btn btn-secondary-outline ms-2" %>
+ <% end %>
+ <% end %>
+ <%= link_to "View", workshop_idea_path(@workshop_idea),
+ class: "btn btn-secondary-outline" %>
+
diff --git a/spec/fixtures/test_image.jpg b/spec/fixtures/test_image.jpg
new file mode 100644
index 000000000..ea680ea0d
Binary files /dev/null and b/spec/fixtures/test_image.jpg differ
diff --git a/spec/services/story_from_idea_service_spec.rb b/spec/services/story_from_idea_service_spec.rb
new file mode 100644
index 000000000..15d8e7878
--- /dev/null
+++ b/spec/services/story_from_idea_service_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe StoryFromIdeaService do
+ subject(:service) { described_class.new(story_idea, user: user) }
+
+ let(:user) { create(:user) }
+ let(:story_idea) { create(:story_idea, created_by: user, updated_by: user) }
+
+ describe "#call" do
+ let(:story) { service.call }
+
+ it "returns a new Story" do
+ expect(story).to be_a(Story)
+ expect(story).to be_new_record
+ end
+
+ it "copies basic attributes from story_idea" do
+ expect(story).to have_attributes(
+ title: story_idea.title,
+ body: story_idea.body,
+ youtube_url: story_idea.youtube_url,
+ windows_type_id: story_idea.windows_type_id,
+ project_id: story_idea.project_id,
+ workshop_id: story_idea.workshop_id,
+ story_idea_id: story_idea.id,
+ created_by_id: user.id,
+ updated_by_id: user.id
+ )
+ end
+
+ it "sets the story as unpublished by default" do
+ expect(story.published).to be false
+ end
+
+ context "when story_idea has a primary_asset" do
+ let(:story_idea) do
+ create(:story_idea, created_by: user, updated_by: user).tap do |si|
+ si.create_primary_asset!(file: fixture_file_upload("spec/fixtures/test_image.jpg", "image/jpeg"))
+ end
+ end
+
+ it "duplicates the primary_asset to the story" do
+ expect(story.primary_asset).to be_present
+ expect(story.primary_asset).to be_a(PrimaryAsset)
+ expect(story.primary_asset.file).to be_attached
+ end
+ end
+
+ context "when story_idea has gallery_assets" do
+ let(:story_idea) do
+ create(:story_idea, created_by: user, updated_by: user).tap do |si|
+ si.gallery_assets.create!(file: fixture_file_upload("spec/fixtures/test_image.jpg", "image/jpeg"))
+ si.gallery_assets.create!(file: fixture_file_upload("spec/fixtures/test_image.jpg", "image/jpeg"))
+ end
+ end
+
+ it "duplicates all gallery_assets to the story" do
+ expect(story.gallery_assets.size).to eq(2)
+ story.gallery_assets.each do |asset|
+ expect(asset).to be_a(GalleryAsset)
+ expect(asset.file).to be_attached
+ end
+ end
+ end
+
+ context "when story_idea has both primary and gallery assets" do
+ let(:story_idea) do
+ create(:story_idea, created_by: user, updated_by: user).tap do |si|
+ si.create_primary_asset!(file: fixture_file_upload("spec/fixtures/test_image.jpg", "image/jpeg"))
+ si.gallery_assets.create!(file: fixture_file_upload("spec/fixtures/test_image.jpg", "image/jpeg"))
+ end
+ end
+
+ it "duplicates both asset types correctly" do
+ expect(story.primary_asset).to be_a(PrimaryAsset)
+ expect(story.gallery_assets.size).to eq(1)
+ expect(story.gallery_assets.first).to be_a(GalleryAsset)
+ end
+ end
+ end
+end
diff --git a/spec/services/workshop_from_idea_service_spec.rb b/spec/services/workshop_from_idea_service_spec.rb
new file mode 100644
index 000000000..835d268e2
--- /dev/null
+++ b/spec/services/workshop_from_idea_service_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe WorkshopFromIdeaService do
+ subject(:service) { described_class.new(workshop_idea, user: user) }
+
+ let(:user) { create(:user) }
+ let(:workshop_idea) { create(:workshop_idea, created_by: user, updated_by: user) }
+
+ describe "#call" do
+ let(:workshop) { service.call }
+
+ it "returns a new Workshop" do
+ expect(workshop).to be_a(Workshop)
+ expect(workshop).to be_new_record
+ end
+
+ it "copies basic attributes from workshop_idea" do
+ expect(workshop).to have_attributes(
+ title: workshop_idea.title,
+ objective: workshop_idea.objective,
+ materials: workshop_idea.materials,
+ windows_type_id: workshop_idea.windows_type_id,
+ workshop_idea_id: workshop_idea.id,
+ user_id: user.id
+ )
+ end
+
+ it "sets the workshop as inactive by default" do
+ expect(workshop.inactive).to be true
+ end
+
+ it "sets the workshop as not featured by default" do
+ expect(workshop.featured).to be false
+ end
+
+ context "when workshop_idea has a primary_asset" do
+ let(:workshop_idea) do
+ create(:workshop_idea, created_by: user, updated_by: user).tap do |wi|
+ wi.create_primary_asset!(file: fixture_file_upload("spec/fixtures/test_image.jpg", "image/jpeg"))
+ end
+ end
+
+ it "duplicates the primary_asset to the workshop" do
+ expect(workshop.primary_asset).to be_present
+ expect(workshop.primary_asset).to be_a(PrimaryAsset)
+ expect(workshop.primary_asset.file).to be_attached
+ end
+ end
+
+ context "when workshop_idea has gallery_assets" do
+ let(:workshop_idea) do
+ create(:workshop_idea, created_by: user, updated_by: user).tap do |wi|
+ wi.gallery_assets.create!(file: fixture_file_upload("spec/fixtures/test_image.jpg", "image/jpeg"))
+ wi.gallery_assets.create!(file: fixture_file_upload("spec/fixtures/test_image.jpg", "image/jpeg"))
+ end
+ end
+
+ it "duplicates all gallery_assets to the workshop" do
+ expect(workshop.gallery_assets.size).to eq(2)
+ workshop.gallery_assets.each do |asset|
+ expect(asset).to be_a(GalleryAsset)
+ expect(asset.file).to be_attached
+ end
+ end
+ end
+
+ context "when workshop_idea has both primary and gallery assets" do
+ let(:workshop_idea) do
+ create(:workshop_idea, created_by: user, updated_by: user).tap do |wi|
+ wi.create_primary_asset!(file: fixture_file_upload("spec/fixtures/test_image.jpg", "image/jpeg"))
+ wi.gallery_assets.create!(file: fixture_file_upload("spec/fixtures/test_image.jpg", "image/jpeg"))
+ end
+ end
+
+ it "duplicates both asset types correctly" do
+ expect(workshop.primary_asset).to be_a(PrimaryAsset)
+ expect(workshop.gallery_assets.size).to eq(1)
+ expect(workshop.gallery_assets.first).to be_a(GalleryAsset)
+ end
+ end
+ end
+end