diff --git a/app/controllers/workshop_variation_ideas_controller.rb b/app/controllers/workshop_variation_ideas_controller.rb new file mode 100644 index 000000000..4409b59f2 --- /dev/null +++ b/app/controllers/workshop_variation_ideas_controller.rb @@ -0,0 +1,81 @@ +class WorkshopVariationIdeasController < ApplicationController + before_action :set_workshop_variation_idea, only: [ :show, :edit, :update, :destroy ] + + def index + per_page = params[:number_of_items_per_page].presence || 25 + workshop_variation_ideas = WorkshopVariationIdea.includes(:workshop, :created_by, :updated_by) + @workshop_variation_ideas_count = workshop_variation_ideas.size + @workshop_variation_ideas = workshop_variation_ideas.order(created_at: :desc) + .paginate(page: params[:page], per_page: per_page) + .decorate + end + + def show + end + + def new + @workshop_variation_idea = WorkshopVariationIdea.new + set_form_variables + end + + def edit + set_form_variables + end + + def create + @workshop_variation_idea = WorkshopVariationIdea.new(workshop_variation_idea_params) + + if @workshop_variation_idea.save + NotificationServices::CreateNotification.call( + noticeable: @workshop_variation_idea, + kind: :idea_submitted_fyi, + recipient_role: :admin, + recipient_email: ENV.fetch("REPLY_TO_EMAIL", "programs@awbw.org"), + notification_type: 0) + redirect_to workshop_variation_ideas_path, notice: "Workshop variation idea was successfully created." + else + set_form_variables + render :new, status: :unprocessable_content + end + end + + def update + if @workshop_variation_idea.update(workshop_variation_idea_params) + redirect_to workshop_variation_ideas_path, notice: "Workshop variation idea was successfully updated.", status: :see_other + else + set_form_variables + render :edit, status: :unprocessable_content + end + end + + def destroy + @workshop_variation_idea.destroy! + redirect_to workshop_variation_ideas_path, notice: "Workshop variation idea was successfully destroyed." + end + + private + + def set_workshop_variation_idea + @workshop_variation_idea = WorkshopVariationIdea.find(params[:id]) + end + + def set_form_variables + @workshop_variation_idea.build_primary_asset if @workshop_variation_idea.primary_asset.blank? + @workshop_variation_idea.gallery_assets.build + + @workshops = Workshop.published.order(:title) + @users = User.active.or(User.where(id: @workshop_variation_idea.created_by_id)) + .order(:first_name, :last_name) + end + + def workshop_variation_idea_params + params.require(:workshop_variation_idea).permit( + :name, :description, :youtube_url, + :inactive, :position, + :workshop_id, + :created_by_id, :updated_by_id, + primary_asset_attributes: [ :id, :file, :_destroy ], + gallery_assets_attributes: [ :id, :file, :_destroy ] + ) + end +end diff --git a/app/controllers/workshop_variations_controller.rb b/app/controllers/workshop_variations_controller.rb index a2bc3dbd4..7c6902eb0 100644 --- a/app/controllers/workshop_variations_controller.rb +++ b/app/controllers/workshop_variations_controller.rb @@ -16,7 +16,12 @@ def index end def new - @workshop_variation = WorkshopVariation.new + if params[:workshop_variation_idea_id].present? + @workshop_variation_idea = WorkshopVariationIdea.find(params[:workshop_variation_idea_id]) + @workshop_variation = WorkshopVariationFromIdeaService.new(@workshop_variation_idea, user: current_user).call + else + @workshop_variation = WorkshopVariation.new + end workshops = current_user.super_user? ? Workshop.all : Workshop.published @workshops = workshops.order(:title) @workshop = @workshop_variation.workshop || params[:workshop_id].present? && diff --git a/app/decorators/workshop_variation_idea_decorator.rb b/app/decorators/workshop_variation_idea_decorator.rb new file mode 100644 index 000000000..fe1ef28a4 --- /dev/null +++ b/app/decorators/workshop_variation_idea_decorator.rb @@ -0,0 +1,9 @@ +class WorkshopVariationIdeaDecorator < ApplicationDecorator + def detail(length: nil) + length ? description&.truncate(length) : description + end + + def default_display_image + "workshop_default.jpg" + end +end diff --git a/app/models/workshop_variation.rb b/app/models/workshop_variation.rb index bd7e90a31..a572d218e 100644 --- a/app/models/workshop_variation.rb +++ b/app/models/workshop_variation.rb @@ -3,6 +3,7 @@ class WorkshopVariation < ApplicationRecord belongs_to :workshop belongs_to :created_by, class_name: "User", optional: true + belongs_to :workshop_variation_idea, optional: true has_many :bookmarks, as: :bookmarkable, dependent: :destroy has_many :notifications, as: :noticeable, dependent: :destroy # Asset associations diff --git a/app/models/workshop_variation_idea.rb b/app/models/workshop_variation_idea.rb new file mode 100644 index 000000000..be518e150 --- /dev/null +++ b/app/models/workshop_variation_idea.rb @@ -0,0 +1,32 @@ +class WorkshopVariationIdea < ApplicationRecord + belongs_to :created_by, class_name: "User" + belongs_to :updated_by, class_name: "User" + belongs_to :workshop + has_many :bookmarks, as: :bookmarkable, dependent: :destroy + has_many :notifications, as: :noticeable, dependent: :destroy + has_many :workshop_variations + + # Asset associations + has_one :primary_asset, -> { where(type: "PrimaryAsset") }, + as: :owner, class_name: "PrimaryAsset", dependent: :destroy + has_many :gallery_assets, -> { where(type: "GalleryAsset") }, + as: :owner, class_name: "GalleryAsset", dependent: :destroy + has_many :assets, as: :owner, dependent: :destroy + + # Validations + validates :name, presence: true + validates :created_by_id, presence: true + validates :updated_by_id, presence: true + validates :workshop_id, presence: true + + # 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 + + # Scopes + scope :workshop_id, ->(workshop_id) { where(workshop_id: workshop_id) if workshop_id.present? } + + def title + name + end +end diff --git a/app/services/workshop_variation_from_idea_service.rb b/app/services/workshop_variation_from_idea_service.rb new file mode 100644 index 000000000..503121b7f --- /dev/null +++ b/app/services/workshop_variation_from_idea_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class WorkshopVariationFromIdeaService + def initialize(workshop_variation_idea, user:) + @workshop_variation_idea = workshop_variation_idea + @user = user + end + + def call + WorkshopVariation.new(attributes_from_idea).tap do |workshop_variation| + duplicate_assets(workshop_variation) + end + end + + private + + attr_reader :workshop_variation_idea, :user + + def attributes_from_idea + workshop_variation_idea.attributes.slice( + "name", "description", "youtube_url", + "position", "workshop_id" + ).merge( + created_by_id: user.id, + workshop_variation_idea_id: workshop_variation_idea.id, + inactive: true + ) + end + + def duplicate_assets(workshop_variation) + workshop_variation_idea.assets.each do |asset| + workshop_variation.assets.build(file: asset.file.blob) + end + end +end diff --git a/app/views/workshop_variation_ideas/_form.html.erb b/app/views/workshop_variation_ideas/_form.html.erb new file mode 100644 index 000000000..415e8533c --- /dev/null +++ b/app/views/workshop_variation_ideas/_form.html.erb @@ -0,0 +1,92 @@ +
+ <%= simple_form_for(workshop_variation_idea, html: { multipart: true }) do |f| %> + <%= f.hidden_field :created_by_id, value: f.object.created_by_id || current_user&.id %> + <%= f.hidden_field :updated_by_id, value: current_user&.id %> + + <% promoted_to_variation = f.object.workshop_variations.any? %> + <% if promoted_to_variation %> +
+ This workshop variation idea has been promoted to a + <% f.object.workshop_variations.each do |workshop_variation| %> + <%= link_to "Workshop Variation ##{workshop_variation.id}", + workshop_variation_path(workshop_variation), class: "btn btn-secondary-outline" %> + <% end %> + and should not be edited here. +
+ <% end %> + + <%= render 'shared/errors', resource: workshop_variation_idea if workshop_variation_idea.errors.any? %> + + +
+
+ <%= f.input :name, + label: "Variation name", + required: true, + disabled: promoted_to_variation, + input_html: { class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 #{('readonly' if promoted_to_variation)}" } %> +
+ +
+ <%= f.input :workshop_id, + label: "Workshop", + required: true, + collection: @workshops, + label_method: :title, + value_method: :id, + disabled: promoted_to_variation, + input_html: { class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm #{('readonly' if promoted_to_variation)}" } %> +
+
+ + +
+ <%= f.input :description, + as: :text, + label: "Description", + hint: "Describe how this variation differs from the base workshop", + disabled: promoted_to_variation, + input_html: { rows: 5, class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm #{('readonly' if promoted_to_variation)}" } %> +
+ + +
+
+ <%= f.input :youtube_url, + label: "YouTube URL", + disabled: promoted_to_variation, + input_html: { class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 #{('readonly' if promoted_to_variation)}" } %> +
+ +
+ <%= f.input :position, + label: "Position (order)", + disabled: promoted_to_variation, + input_html: { class: "block w-full rounded-md border-gray-300 shadow-sm focus:ring-blue-500 focus:border-blue-500 #{('readonly' if promoted_to_variation)}" } %> +
+ + <% if current_user.super_user? %> +
+ <%= f.input :inactive, + as: :boolean, + disabled: promoted_to_variation, + wrapper_html: { class: "flex items-center" } %> +
+ <% end %> +
+ + <%= render "shared/form_image_fields", f: f, include_primary_asset: true unless promoted_to_variation %> + + +
+ <% if current_user.super_user? && f.object.persisted? && !promoted_to_variation %> + <%= link_to "Promote to Workshop Variation", + new_workshop_variation_path(workshop_variation_idea_id: f.object.id), + class: "btn btn-secondary-outline" %> + <%= link_to "Delete", @workshop_variation_idea, class: "btn btn-danger-outline", method: :delete, data: { confirm: "Are you sure?" } %> + <% end %> + <%= link_to "Cancel", workshop_variation_ideas_path, class: "btn btn-secondary-outline" %> + <%= f.submit "Submit", class: "btn btn-primary", disabled: promoted_to_variation %> +
+ <% end %> +
diff --git a/app/views/workshop_variation_ideas/edit.html.erb b/app/views/workshop_variation_ideas/edit.html.erb new file mode 100644 index 000000000..9f6557e31 --- /dev/null +++ b/app/views/workshop_variation_ideas/edit.html.erb @@ -0,0 +1,31 @@ +
+
+
+
+

Edit Workshop Variation Idea

+
+ <% if @workshop_variation_idea.workshop_variations.any? %> + <% @workshop_variation_idea.workshop_variations.each do |workshop_variation| %> + <%= link_to "View Variation", workshop_variation_path(workshop_variation), + class: "btn btn-secondary-outline" %> + <% end %> + <% else %> + <% if current_user.super_user && @workshop_variation_idea.persisted? %> + <%= link_to "Promote to Workshop Variation", + new_workshop_variation_path(workshop_variation_idea_id: @workshop_variation_idea.id), + class: "admin-only bg-blue-100 btn btn-secondary-outline ms-2" %> + <% end %> + <% end %> + <%= link_to "View", workshop_variation_idea_path(@workshop_variation_idea), + class: "btn btn-secondary-outline" %> +
+
+ +
+
+ <%= render "form", workshop_variation_idea: @workshop_variation_idea %> +
+
+
+
+
diff --git a/app/views/workshop_variation_ideas/index.html.erb b/app/views/workshop_variation_ideas/index.html.erb new file mode 100644 index 000000000..6ba02191e --- /dev/null +++ b/app/views/workshop_variation_ideas/index.html.erb @@ -0,0 +1,85 @@ +
+
+
+
+
+ +
+

+ Workshop Variation Ideas (<%= @workshop_variation_ideas_count %>) +

+
+ <%= link_to "New workshop variation idea", + new_workshop_variation_idea_path, + class: "btn btn-primary" %> +
+
+ +
+
+ + + + + + + + + + + + + + <% @workshop_variation_ideas.each do |workshop_variation_idea| %> + + + + + + + + + + + <% end %> + +
Main ImageNameWorkshopAuthorPromoted?Actions
+
+ <%= render "assets/display_image", + resource: workshop_variation_idea, + width: 18, height: 14, + variant: :index, + link_to_object: true, + file: workshop_variation_idea.display_image %> +
+
<%= workshop_variation_idea.name %><%= workshop_variation_idea.workshop.title %><%= workshop_variation_idea.created_by.full_name %> + <% if workshop_variation_idea.workshop_variations.any? %> + + <% else %> + -- + <% end %> + + <%= link_to 'Edit', edit_workshop_variation_idea_path(workshop_variation_idea), class: "btn btn-secondary-outline" %> +
+
+
+ + + <% unless @workshop_variation_ideas.any? %> +

+ No workshop variation ideas found. +

+ <% end %> + + +
+ +
+
+
+
+
+
diff --git a/app/views/workshop_variation_ideas/new.html.erb b/app/views/workshop_variation_ideas/new.html.erb new file mode 100644 index 000000000..075b3d052 --- /dev/null +++ b/app/views/workshop_variation_ideas/new.html.erb @@ -0,0 +1,18 @@ +
+
+
+
+

+ New Workshop Variation Idea +

+
+ +
+
+ <%= render "form", workshop_variation_idea: @workshop_variation_idea %> +
+
+
+
+
diff --git a/app/views/workshop_variation_ideas/show.html.erb b/app/views/workshop_variation_ideas/show.html.erb new file mode 100644 index 000000000..b1e50b406 --- /dev/null +++ b/app/views/workshop_variation_ideas/show.html.erb @@ -0,0 +1,58 @@ +
+
+
+
+ +

+ Workshop Variation Idea Details +

+ + +
+ <%= link_to("Index", workshop_variation_ideas_path, class: "btn btn-secondary-outline") %> + <% if current_user.super_user? %> + <%= link_to("Edit", edit_workshop_variation_idea_path(@workshop_variation_idea), class: "btn btn-primary-outline") %> + <% end %> +
+
+ +
+
+
+
+

Name:

+

<%= @workshop_variation_idea.name %>

+
+
+

Workshop:

+

<%= @workshop_variation_idea.workshop.title %>

+
+
+

Description:

+

<%= @workshop_variation_idea.description %>

+
+
+

YouTube URL:

+

<%= @workshop_variation_idea.youtube_url %>

+
+
+

Created by:

+

<%= @workshop_variation_idea.created_by.full_name %>

+
+
+

Position:

+

<%= @workshop_variation_idea.position %>

+
+
+

Inactive:

+

<%= @workshop_variation_idea.inactive ? "Yes" : "No" %>

+
+
+
+
+
+ <%= render "assets/display_assets", resource: @workshop_variation_idea %> +
+
+
+
diff --git a/config/routes.rb b/config/routes.rb index a888cd316..efd35e679 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -98,6 +98,7 @@ resources :workshop_ideas resources :workshop_logs resources :workshop_log_creation_wizard + resources :workshop_variation_ideas resources :workshop_variations resources :workshops do collection do diff --git a/db/migrate/20260120011334_create_workshop_variation_ideas.rb b/db/migrate/20260120011334_create_workshop_variation_ideas.rb new file mode 100644 index 000000000..14fca4bfe --- /dev/null +++ b/db/migrate/20260120011334_create_workshop_variation_ideas.rb @@ -0,0 +1,18 @@ +class CreateWorkshopVariationIdeas < ActiveRecord::Migration[8.0] + def change + create_table :workshop_variation_ideas do |t| + t.string :name, null: false + t.text :description, size: :long + t.string :youtube_url + t.boolean :inactive, default: true + t.integer :position + t.references :workshop, null: false, foreign_key: true + t.references :created_by, null: false, foreign_key: { to_table: :users } + t.references :updated_by, null: false, foreign_key: { to_table: :users } + + t.timestamps + end + + add_index :workshop_variation_ideas, :name + end +end diff --git a/db/migrate/20260120011347_add_workshop_variation_idea_to_workshop_variations.rb b/db/migrate/20260120011347_add_workshop_variation_idea_to_workshop_variations.rb new file mode 100644 index 000000000..817e8f133 --- /dev/null +++ b/db/migrate/20260120011347_add_workshop_variation_idea_to_workshop_variations.rb @@ -0,0 +1,5 @@ +class AddWorkshopVariationIdeaToWorkshopVariations < ActiveRecord::Migration[8.0] + def change + add_reference :workshop_variations, :workshop_variation_idea, foreign_key: true + end +end diff --git a/spec/factories/workshop_variation_ideas.rb b/spec/factories/workshop_variation_ideas.rb new file mode 100644 index 000000000..9e19e95fd --- /dev/null +++ b/spec/factories/workshop_variation_ideas.rb @@ -0,0 +1,13 @@ +FactoryBot.define do + factory :workshop_variation_idea do + name { "Workshop Variation Idea" } + description { "This is a variation idea description" } + youtube_url { "https://www.youtube.com/watch?v=example" } + inactive { true } + position { 1 } + + association :workshop + association :created_by, factory: :user + association :updated_by, factory: :user + end +end