diff --git a/app/controllers/home/video_gallery_controller.rb b/app/controllers/home/video_gallery_controller.rb
deleted file mode 100644
index 8357fc504..000000000
--- a/app/controllers/home/video_gallery_controller.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Home
- class VideoGalleryController < ApplicationController
- skip_before_action :authenticate_user!
-
- def index
- authorize! :home
- base = Tutorial.where.not(youtube_url: [ nil, "" ]).order(position: :asc, created_at: :desc)
- @tutorials = authorized_scope(base, with: HomePolicy).decorate
-
- render "home/video_gallery/index"
- end
- end
-end
diff --git a/app/controllers/home/video_library_controller.rb b/app/controllers/home/video_library_controller.rb
new file mode 100644
index 000000000..55a27ab8d
--- /dev/null
+++ b/app/controllers/home/video_library_controller.rb
@@ -0,0 +1,13 @@
+module Home
+ class VideoLibraryController < ApplicationController
+ skip_before_action :authenticate_user!
+
+ def index
+ authorize! :home
+ base = VideoLibrary.where.not(youtube_url: [ nil, "" ]).order(position: :asc, created_at: :desc)
+ @tutorials = authorized_scope(base, with: HomePolicy).decorate
+
+ render "home/video_library/index"
+ end
+ end
+end
diff --git a/app/controllers/tutorials_controller.rb b/app/controllers/tutorials_controller.rb
index aca8c3f7d..c85741d7a 100644
--- a/app/controllers/tutorials_controller.rb
+++ b/app/controllers/tutorials_controller.rb
@@ -22,6 +22,25 @@ def index
end
end
+ def video_library
+ authorize!
+ if turbo_frame_request?
+ per_page = params[:number_of_items_per_page].presence || 6
+ base_scope = authorized_scope(VideoLibrary.all)
+ filtered = base_scope.search_by_params(params)
+
+ @count_display = filtered.size == base_scope.size ? base_scope.size : "#{filtered.count}/#{base_scope.count}"
+ @video_library = filtered.order(:position).paginate(page: params[:page], per_page: per_page).decorate
+
+ render :video_library_lazy
+ else
+ @sectors = Sector.published.order(:name)
+ @category_types = CategoryType.published.general.order(:name).decorate
+
+ render :video_library
+ end
+ end
+
def show
@tutorial = @tutorial.decorate
authorize! @tutorial
@@ -119,7 +138,7 @@ def set_tutorial
# Strong parameters
def tutorial_params
params.require(:tutorial).permit(
- :title, :body, :rhino_body, :position, :youtube_url,
+ :title, :body, :rhino_body, :position, :youtube_url, :type,
:featured, :published, :publicly_visible, :publicly_featured,
category_ids: [],
sector_ids: [],
diff --git a/app/models/tutorial.rb b/app/models/tutorial.rb
index 6d48451a6..cc8847327 100644
--- a/app/models/tutorial.rb
+++ b/app/models/tutorial.rb
@@ -1,4 +1,6 @@
-class Tutorial < ApplicationRecord
+class VideoLibrary < ApplicationRecord
+ self.table_name = 'tutorials'
+
include Featureable, Publishable, TagFilterable, Trendable, RichTextSearchable
has_rich_text :rhino_body
@@ -9,9 +11,9 @@ class Tutorial < ApplicationRecord
has_many :categories, through: :categorizable_items
has_many :sectors, through: :sectorable_items
# Asset associations
- has_one :primary_asset, -> { where(type: "PrimaryAsset") },
+ has_one :primary_asset, -> { where("type" => "PrimaryAsset") },
as: :owner, class_name: "PrimaryAsset", dependent: :destroy
- has_many :gallery_assets, -> { where(type: "GalleryAsset") },
+ has_many :gallery_assets, -> { where("type" => "GalleryAsset") },
as: :owner, class_name: "GalleryAsset", dependent: :destroy
has_many :assets, as: :owner, dependent: :destroy
@@ -34,11 +36,14 @@ class Tutorial < ApplicationRecord
scope :body, ->(body) { where("body like ?", "%#{ body }%") }
scope :title, ->(title) { where("title like ?", "%#{ title }%") }
scope :tutorial_name, ->(tutorial_name) { title(tutorial_name) }
+ scope :tutorials, -> { where(type: 'Tutorial') }
+ scope :podcasts, -> { where(type: 'Podcast') }
+ scope :intros, -> { where(type: 'Intro') }
scope :with_sector_ids, ->(sector_hash) {
ids = sector_hash.values.reject(&:blank?).map(&:to_i)
return all if ids.empty?
joins(:sectorable_items)
- .where(sectorable_items: { sectorable_type: "Tutorial", sector_id: ids })
+ .where(sectorable_items: { sectorable_type: "VideoLibrary", sector_id: ids })
.distinct
}
@@ -46,7 +51,7 @@ class Tutorial < ApplicationRecord
ids = category_hash.values.reject(&:blank?).map(&:to_i)
return all if ids.empty?
joins(:categorizable_items)
- .where(categorizable_items: { categorizable_type: "Tutorial", category_id: ids })
+ .where(categorizable_items: { categorizable_type: "VideoLibrary", category_id: ids })
.distinct
}
@@ -73,3 +78,12 @@ def self.search_by_params(params)
resources
end
end
+
+class Tutorial < VideoLibrary
+end
+
+class Podcast < VideoLibrary
+end
+
+class Intro < VideoLibrary
+end
diff --git a/app/views/home/video_gallery/index.html.erb b/app/views/home/video_library/index.html.erb
similarity index 100%
rename from app/views/home/video_gallery/index.html.erb
rename to app/views/home/video_library/index.html.erb
diff --git a/app/views/shared/_navbar_menu.html.erb b/app/views/shared/_navbar_menu.html.erb
index a1721b231..6b2e1cc26 100644
--- a/app/views/shared/_navbar_menu.html.erb
+++ b/app/views/shared/_navbar_menu.html.erb
@@ -94,6 +94,12 @@
Organizations
<% end %>
<% end %>
+
+ <%= link_to video_library_path,
+ class: "flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" do %>
+
+ Video Library
+ <% end %>
diff --git a/app/views/shared/_navbar_menu_mobile.html.erb b/app/views/shared/_navbar_menu_mobile.html.erb
index c1cc41839..331ec669e 100644
--- a/app/views/shared/_navbar_menu_mobile.html.erb
+++ b/app/views/shared/_navbar_menu_mobile.html.erb
@@ -76,6 +76,12 @@
Organizations
<% end %>
<% end %>
+
+ <%= link_to video_library_path, class: "flex items-center px-4 py-2 text-sm text-white
+ hover:text-gray-700 hover:bg-gray-100 w-full space-x-2" do %>
+
+ Video Library
+ <% end %>
diff --git a/app/views/tutorials/_form.html.erb b/app/views/tutorials/_form.html.erb
index a2c17caa6..a96bc5d92 100644
--- a/app/views/tutorials/_form.html.erb
+++ b/app/views/tutorials/_form.html.erb
@@ -9,6 +9,7 @@
value: f.object.title } %>
+ <%= f.input :type, as: :select, collection: ['Tutorial', 'Podcast', 'Intro'], prompt: 'Select type' %>
<%= f.input :published, as: :boolean %>
<%= f.input :featured, as: :boolean %>
<%= f.input :publicly_visible, as: :boolean %>
diff --git a/app/views/tutorials/video_library.html.erb b/app/views/tutorials/video_library.html.erb
new file mode 100644
index 000000000..6f9e8e5a0
--- /dev/null
+++ b/app/views/tutorials/video_library.html.erb
@@ -0,0 +1,40 @@
+<% content_for(:page_bg_class, "public") %>
+<%= render "shared/public_welcome_banner" %>
+
+
+
+
+
+
+ Video Library
+
+
+
+ Explore our collection of tutorials, podcasts, and introductory videos
+
+
+
+
+ <% if allowed_to?(:new?, Tutorial) %>
+ <%= link_to "New #{Tutorial.model_name.human.downcase}",
+ new_tutorial_path,
+ class: "admin-only bg-blue-100 btn btn-primary-outline" %>
+ <% end %>
+
+
+
+ <%= render "search_boxes" %>
+
+ <% result_src = video_library_path + "?" + request.query_string %>
+
+ <%= turbo_frame_tag "video_library_results", src: result_src do %>
+
+
+ <% 3.times do %>
+
+ <% end %>
+
+
+ <% end %>
+
+
diff --git a/app/views/tutorials/video_library_lazy.html.erb b/app/views/tutorials/video_library_lazy.html.erb
new file mode 100644
index 000000000..820e3a899
--- /dev/null
+++ b/app/views/tutorials/video_library_lazy.html.erb
@@ -0,0 +1,22 @@
+<%= turbo_frame_tag "video_library_results" do %>
+
+
+
+ <% if @video_library.any? %>
+ <% @video_library.each_with_index do |item, idx| %>
+ <%= render "tutorial", tutorial: item, show_divider: idx > 0 %>
+ <% end %>
+
+
+
+ <% else %>
+
+ No videos found matching your filters.
+
+ <% end %>
+
+
+
+<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 0c7b7680f..52fc6609e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -148,6 +148,7 @@
resources :stories
resources :story_shares, only: [ :index, :show ]
resources :tutorials
+ get "video_library", to: "tutorials#video_library", as: :video_library
resources :user_forms
resources :windows_types
resources :workshop_ideas
@@ -176,7 +177,7 @@
resources :stories, only: :index
resources :community_news, only: :index
resources :events, only: :index
- resources :video_gallery, only: :index
+ resources :video_library, only: :index
end
root to: "home#index"
diff --git a/db/migrate/20260228174902_add_type_to_tutorials_for_sti.rb b/db/migrate/20260228174902_add_type_to_tutorials_for_sti.rb
new file mode 100644
index 000000000..b96359eeb
--- /dev/null
+++ b/db/migrate/20260228174902_add_type_to_tutorials_for_sti.rb
@@ -0,0 +1,6 @@
+class AddTypeToTutorialsForSti < ActiveRecord::Migration[8.0]
+ def change
+ add_column :tutorials, :type, :string, default: 'Tutorial', null: false
+ add_index :tutorials, :type
+ end
+end
diff --git a/spec/factories/tutorials.rb b/spec/factories/tutorials.rb
index 87c630367..66cc35970 100644
--- a/spec/factories/tutorials.rb
+++ b/spec/factories/tutorials.rb
@@ -1,11 +1,12 @@
FactoryBot.define do
- factory :tutorial do
+ factory :video_library do
title { "MyString" }
body { "MyText" }
featured { false }
published { false }
position { 1 }
youtube_url { "MyString" }
+ type { 'Tutorial' }
trait :featured do
featured { true }
@@ -26,5 +27,29 @@
trait :publicly_featured do
publicly_featured { true }
end
+
+ trait :tutorial do
+ type { 'Tutorial' }
+ end
+
+ trait :podcast do
+ type { 'Podcast' }
+ end
+
+ trait :intro do
+ type { 'Intro' }
+ end
+ end
+
+ factory :tutorial, class: 'Tutorial', parent: :video_library do
+ type { 'Tutorial' }
+ end
+
+ factory :podcast, class: 'Podcast', parent: :video_library do
+ type { 'Podcast' }
+ end
+
+ factory :intro, class: 'Intro', parent: :video_library do
+ type { 'Intro' }
end
end
diff --git a/spec/models/tutorial_spec.rb b/spec/models/tutorial_spec.rb
index 1872dc491..76c006406 100644
--- a/spec/models/tutorial_spec.rb
+++ b/spec/models/tutorial_spec.rb
@@ -40,4 +40,40 @@
expect(results).not_to include(draft_tutorial)
end
end
+
+ describe '.tutorials' do
+ let!(:tutorial) { create(:tutorial, title: 'Tutorial 1') }
+ let!(:podcast) { create(:podcast, title: 'Podcast 1') }
+ let!(:intro) { create(:intro, title: 'Intro 1') }
+
+ it 'returns only Tutorial type records' do
+ results = VideoLibrary.tutorials
+ expect(results).to include(tutorial)
+ expect(results).not_to include(podcast, intro)
+ end
+ end
+
+ describe '.podcasts' do
+ let!(:tutorial) { create(:tutorial, title: 'Tutorial 1') }
+ let!(:podcast) { create(:podcast, title: 'Podcast 1') }
+ let!(:intro) { create(:intro, title: 'Intro 1') }
+
+ it 'returns only Podcast type records' do
+ results = VideoLibrary.podcasts
+ expect(results).to include(podcast)
+ expect(results).not_to include(tutorial, intro)
+ end
+ end
+
+ describe '.intros' do
+ let!(:tutorial) { create(:tutorial, title: 'Tutorial 1') }
+ let!(:podcast) { create(:podcast, title: 'Podcast 1') }
+ let!(:intro) { create(:intro, title: 'Intro 1') }
+
+ it 'returns only Intro type records' do
+ results = VideoLibrary.intros
+ expect(results).to include(intro)
+ expect(results).not_to include(tutorial, podcast)
+ end
+ end
end
diff --git a/spec/requests/home/video_library_spec.rb b/spec/requests/home/video_library_spec.rb
new file mode 100644
index 000000000..4e275e61b
--- /dev/null
+++ b/spec/requests/home/video_library_spec.rb
@@ -0,0 +1,39 @@
+require "rails_helper"
+
+RSpec.describe "Home::VideoLibrary", type: :request do
+ describe "GET /home/video_library" do
+ it "renders a successful response" do
+ get home_video_library_url
+ expect(response).to be_successful
+ end
+
+ it "displays all content types with youtube_url" do
+ tutorial = create(:tutorial, title: 'Tutorial Video', youtube_url: 'https://www.youtube.com/watch?v=1')
+ podcast = create(:podcast, title: 'Podcast Video', youtube_url: 'https://www.youtube.com/watch?v=2')
+ intro = create(:intro, title: 'Intro Video', youtube_url: 'https://www.youtube.com/watch?v=3')
+ get home_video_library_url
+ expect(response.body).to include(tutorial.title, podcast.title, intro.title)
+ end
+
+ it "excludes records without youtube_url" do
+ with_url = create(:tutorial, title: 'With URL', youtube_url: 'https://www.youtube.com/watch?v=1')
+ without_url = create(:podcast, title: 'No URL', youtube_url: nil)
+ get home_video_library_url
+ expect(response.body).to include(with_url.title)
+ expect(response.body).not_to include(without_url.title)
+ end
+
+ it "orders by position ascending, then created_at descending" do
+ item1 = create(:tutorial, position: 2, youtube_url: 'https://www.youtube.com/watch?v=1')
+ item2 = create(:podcast, position: 1, youtube_url: 'https://www.youtube.com/watch?v=2')
+ item3 = create(:intro, position: 1, youtube_url: 'https://www.youtube.com/watch?v=3')
+ get home_video_library_url
+ body = response.body
+ pos2 = body.index(item2.title)
+ pos3 = body.index(item3.title)
+ pos1 = body.index(item1.title)
+ expect(pos2).to be < pos3 unless pos2.nil? || pos3.nil?
+ expect(pos2).to be < pos1 unless pos2.nil? || pos1.nil?
+ end
+ end
+end
diff --git a/spec/requests/tutorials_spec.rb b/spec/requests/tutorials_spec.rb
index 17391497e..fa82d0f9a 100644
--- a/spec/requests/tutorials_spec.rb
+++ b/spec/requests/tutorials_spec.rb
@@ -44,6 +44,31 @@
get tutorials_url
expect(response).to be_successful
end
+
+ it "shows only Tutorial type records" do
+ tutorial = create(:tutorial, title: 'Test Tutorial')
+ podcast = create(:podcast, title: 'Test Podcast')
+ intro = create(:intro, title: 'Test Intro')
+ get tutorials_url
+ expect(response.body).to include(tutorial.title)
+ expect(response.body).not_to include(podcast.title, intro.title)
+ end
+ end
+
+ describe "GET /video_library" do
+ it "renders a successful response" do
+ create(:tutorial)
+ get video_library_url
+ expect(response).to be_successful
+ end
+
+ it "shows all content types (Tutorial, Podcast, Intro)" do
+ tutorial = create(:tutorial, title: 'Test Tutorial')
+ podcast = create(:podcast, title: 'Test Podcast')
+ intro = create(:intro, title: 'Test Intro')
+ get video_library_url
+ expect(response.body).to include(tutorial.title, podcast.title, intro.title)
+ end
end
describe "GET /show" do
diff --git a/spec/routing/tutorials_routing_spec.rb b/spec/routing/tutorials_routing_spec.rb
index 61624b99e..77d2b81d0 100644
--- a/spec/routing/tutorials_routing_spec.rb
+++ b/spec/routing/tutorials_routing_spec.rb
@@ -34,5 +34,9 @@
it "routes to #destroy" do
expect(delete: "/tutorials/1").to route_to("tutorials#destroy", id: "1")
end
+
+ it "routes /video_library to tutorials#video_library" do
+ expect(get: "/video_library").to route_to("tutorials#video_library")
+ end
end
end