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