From 2d3d4cd1b8189a1020230bf9cbe00193e18f3587 Mon Sep 17 00:00:00 2001 From: Dylan Will <58506115+ChargrilledChook@users.noreply.github.com> Date: Tue, 2 Jan 2024 12:24:12 +1100 Subject: [PATCH 1/2] Backend: Basic plumbing for bookmark feature Add basic bookmark component Add routes Add title delegator to bookmark model Add basic bookmark controller Add bookmark to lessons controller Add first pass basic bookmark button component Placeholder basic bookmark index page Add bookmark component to lesson buttons Get button working roughly with turbo stream --- .../bookmarks/button_component.html.erb | 10 ++++ app/components/bookmarks/button_component.rb | 29 +++++++++++ app/controllers/lessons_controller.rb | 16 +++++- app/controllers/users/bookmarks_controller.rb | 50 +++++++++++++++++++ app/models/bookmark.rb | 8 +++ app/models/lesson.rb | 1 + app/models/user.rb | 2 + app/views/lessons/_lesson_buttons.html.erb | 1 + app/views/lessons/show.html.erb | 2 +- .../users/bookmarks/create.turbo_stream.erb | 5 ++ .../users/bookmarks/destroy.turbo_stream.erb | 5 ++ app/views/users/bookmarks/index.html.erb | 16 ++++++ config/routes.rb | 1 + db/migrate/20231231013215_create_bookmarks.rb | 12 +++++ db/schema.rb | 14 +++++- spec/factories/bookmarks.rb | 6 +++ spec/models/bookmark_spec.rb | 23 +++++++++ spec/models/lesson_spec.rb | 1 + spec/models/user_spec.rb | 2 + 19 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 app/components/bookmarks/button_component.html.erb create mode 100644 app/components/bookmarks/button_component.rb create mode 100644 app/controllers/users/bookmarks_controller.rb create mode 100644 app/models/bookmark.rb create mode 100644 app/views/users/bookmarks/create.turbo_stream.erb create mode 100644 app/views/users/bookmarks/destroy.turbo_stream.erb create mode 100644 app/views/users/bookmarks/index.html.erb create mode 100644 db/migrate/20231231013215_create_bookmarks.rb create mode 100644 spec/factories/bookmarks.rb create mode 100644 spec/models/bookmark_spec.rb diff --git a/app/components/bookmarks/button_component.html.erb b/app/components/bookmarks/button_component.html.erb new file mode 100644 index 0000000000..771f6a398a --- /dev/null +++ b/app/components/bookmarks/button_component.html.erb @@ -0,0 +1,10 @@ +
+ <%= button_to create_or_destroy_path, + form_class: 'w-full h-full', + method: bookmarked? ? :delete : :post, + data: { test_id: 'bookmark-button' }, + params: { lesson_id: lesson.id }, + class: 'button button--secondary h-[54px] sm:h-full w-full hover:bg-teal-700' do %> + + <% end %> +
diff --git a/app/components/bookmarks/button_component.rb b/app/components/bookmarks/button_component.rb new file mode 100644 index 0000000000..eb6e88c375 --- /dev/null +++ b/app/components/bookmarks/button_component.rb @@ -0,0 +1,29 @@ +class Bookmarks::ButtonComponent < ApplicationComponent + def initialize(lesson:, bookmark:, current_user: nil) + @lesson = lesson + @current_user = current_user + @bookmark = bookmark + end + + private + + attr_reader :lesson, :current_user, :bookmark + + def render? + return false unless current_user + + Feature.enabled?(:bookmarks, current_user) + end + + def bookmarked? + bookmark.present? + end + + def icon + bookmarked? ? 'fa-solid fa-bookmark' : 'fa-regular fa-bookmark' + end + + def create_or_destroy_path + bookmarked? ? users_bookmark_path(bookmark) : users_bookmarks_path + end +end diff --git a/app/controllers/lessons_controller.rb b/app/controllers/lessons_controller.rb index 79cf8a1bb1..032c36bd46 100644 --- a/app/controllers/lessons_controller.rb +++ b/app/controllers/lessons_controller.rb @@ -1,11 +1,23 @@ class LessonsController < ApplicationController before_action :set_cache_control_header_to_no_store + before_action :set_lesson + before_action :set_bookmark def show - @lesson = Lesson.find(params[:id]) - if user_signed_in? Courses::MarkCompletedLessons.call(user: current_user, lessons: Array(@lesson)) end end + + private + + def set_lesson + @lesson = Lesson.find(params[:id]) + end + + def set_bookmark + return unless Feature.enabled?(:bookmarks, current_user) + + @bookmark = current_user.bookmarks.find_by(lesson_id: @lesson.id) + end end diff --git a/app/controllers/users/bookmarks_controller.rb b/app/controllers/users/bookmarks_controller.rb new file mode 100644 index 0000000000..212184276c --- /dev/null +++ b/app/controllers/users/bookmarks_controller.rb @@ -0,0 +1,50 @@ +class Users::BookmarksController < ApplicationController + before_action :authenticate_user! + before_action :set_lesson, only: %i[create destroy] + + def index + @bookmarks = current_user.bookmarks + @lessons = current_user.bookmarked_lessons + end + + def new + @bookmark = Bookmark.new + end + + def create + respond_to do |format| + @bookmark = current_user.bookmarks.build(lesson: @lesson) + + if @bookmark.save + format.turbo_stream { flash.now[:notice] = create_or_destroy_bookmark_helper } + format.html { redirect_to({ action: 'index' }, notice: create_or_destroy_bookmark_helper) } + else + format.html { redirect_back status: :unprocessable_entity, alert: 'Unable to create bookmark' } + end + end + end + + def destroy + respond_to do |format| + @bookmark = Bookmark.find(params[:id]) + @bookmark.destroy + + format.turbo_stream { flash.now[:notice] = create_or_destroy_bookmark_helper(destroy: true) } + format.html { redirect_to({ action: 'index' }, notice: create_or_destroy_bookmark_helper(destroy: true)) } + end + end + + private + + def set_lesson + @lesson = Lesson.find(params[:lesson_id]) + end + + # HACK: Temp method for easier prototyping + def create_or_destroy_bookmark_helper(destroy: false) + <<~FLASH.html_safe # rubocop:disable Rails/OutputSafety + Bookmark #{destroy ? 'removed' : 'created'}! + #{helpers.link_to 'Click to see saved bookmarks', users_bookmarks_path} + FLASH + end +end diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb new file mode 100644 index 0000000000..50de6d0320 --- /dev/null +++ b/app/models/bookmark.rb @@ -0,0 +1,8 @@ +class Bookmark < ApplicationRecord + belongs_to :user + belongs_to :lesson + + validates :lesson, uniqueness: { scope: :user, message: 'user has already bookmarked this lesson' } + + delegate :title, to: :lesson +end diff --git a/app/models/lesson.rb b/app/models/lesson.rb index eab9bc26fa..b6904878c4 100644 --- a/app/models/lesson.rb +++ b/app/models/lesson.rb @@ -9,6 +9,7 @@ class Lesson < ApplicationRecord has_one :content, dependent: :destroy has_many :project_submissions, dependent: :destroy has_many :lesson_completions, dependent: :destroy + has_many :bookmarks, dependent: :destroy has_many :completing_users, through: :lesson_completions, source: :user scope :most_recent_updated_at, -> { maximum(:updated_at) } diff --git a/app/models/user.rb b/app/models/user.rb index 3781bdd93f..d18b1557f8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,6 +11,8 @@ class User < ApplicationRecord has_many :lesson_completions, dependent: :destroy has_many :completed_lessons, through: :lesson_completions, source: :lesson + has_many :bookmarks, dependent: :destroy + has_many :bookmarked_lessons, through: :bookmarks, source: :lesson has_many :project_submissions, dependent: :destroy has_many :user_providers, dependent: :destroy has_many :flags, foreign_key: :flagger_id, dependent: :destroy, inverse_of: :flagger diff --git a/app/views/lessons/_lesson_buttons.html.erb b/app/views/lessons/_lesson_buttons.html.erb index b9a795a9a6..572faec9eb 100644 --- a/app/views/lessons/_lesson_buttons.html.erb +++ b/app/views/lessons/_lesson_buttons.html.erb @@ -5,6 +5,7 @@ <% end %> <% if user_signed_in? %> + <%= render Bookmarks::ButtonComponent.new(lesson:, bookmark:, current_user:) %> <%= render Complete::ButtonComponent.new(lesson:) %> <% else %> <%= link_to( diff --git a/app/views/lessons/show.html.erb b/app/views/lessons/show.html.erb index b03cba947e..35c9d0f2ff 100644 --- a/app/views/lessons/show.html.erb +++ b/app/views/lessons/show.html.erb @@ -46,7 +46,7 @@

<% end %> - <%= render 'lesson_buttons', lesson: @lesson, course: @lesson.course, user: @user %> + <%= render 'lesson_buttons', lesson: @lesson, course: @lesson.course, user: @user, bookmark: @bookmark %> <% end %> <% end %> diff --git a/app/views/users/bookmarks/create.turbo_stream.erb b/app/views/users/bookmarks/create.turbo_stream.erb new file mode 100644 index 0000000000..5c9ef616c1 --- /dev/null +++ b/app/views/users/bookmarks/create.turbo_stream.erb @@ -0,0 +1,5 @@ +<%= turbo_stream.replace 'bookmark-button' do %> + <%= render Bookmarks::ButtonComponent.new(lesson: @lesson, current_user:, bookmark: @bookmark) %> +<% end %> + +<%= turbo_stream.update 'flash-messages', partial: 'shared/flash' %> diff --git a/app/views/users/bookmarks/destroy.turbo_stream.erb b/app/views/users/bookmarks/destroy.turbo_stream.erb new file mode 100644 index 0000000000..58b883772a --- /dev/null +++ b/app/views/users/bookmarks/destroy.turbo_stream.erb @@ -0,0 +1,5 @@ +<%= turbo_stream.replace 'bookmark-button' do %> + <%= render Bookmarks::ButtonComponent.new(lesson: @lesson, current_user:, bookmark: nil) %> +<% end %> + +<%= turbo_stream.update 'flash-messages', partial: 'shared/flash' %> diff --git a/app/views/users/bookmarks/index.html.erb b/app/views/users/bookmarks/index.html.erb new file mode 100644 index 0000000000..7dcbbbf32a --- /dev/null +++ b/app/views/users/bookmarks/index.html.erb @@ -0,0 +1,16 @@ + +
+
+

My Bookmarked Lessons

+
+
+ <% @lessons.each do|lesson| %> +
+ + <%= link_to lesson.title, lesson %> +
+ <% end %> +
+
+
+
diff --git a/config/routes.rb b/config/routes.rb index b67b578f8d..d73a0fa708 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -77,6 +77,7 @@ resources :progress, only: :destroy resources :project_submissions, only: %i[edit update] resource :profile, only: %i[edit update] + resources :bookmarks, only: %i[index new create destroy] end namespace :lessons do diff --git a/db/migrate/20231231013215_create_bookmarks.rb b/db/migrate/20231231013215_create_bookmarks.rb new file mode 100644 index 0000000000..32a56fff5f --- /dev/null +++ b/db/migrate/20231231013215_create_bookmarks.rb @@ -0,0 +1,12 @@ +class CreateBookmarks < ActiveRecord::Migration[7.0] + def change + create_table :bookmarks do |t| + t.belongs_to :lesson, null: false, foreign_key: true + t.belongs_to :user, null: false, foreign_key: true + + t.timestamps + + t.index %i[user_id lesson_id], unique: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 20fba2b04e..1d0040e839 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_08_05_091337) do +ActiveRecord::Schema[7.0].define(version: 2023_12_31_013215) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -40,6 +40,16 @@ t.index ["user_id"], name: "index_announcements_on_user_id" end + create_table "bookmarks", force: :cascade do |t| + t.bigint "lesson_id", null: false + t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["lesson_id"], name: "index_bookmarks_on_lesson_id" + t.index ["user_id", "lesson_id"], name: "index_bookmarks_on_user_id_and_lesson_id", unique: true + t.index ["user_id"], name: "index_bookmarks_on_user_id" + end + create_table "contents", force: :cascade do |t| t.text "body", null: false t.bigint "lesson_id", null: false @@ -283,6 +293,8 @@ end add_foreign_key "announcements", "users" + add_foreign_key "bookmarks", "lessons" + add_foreign_key "bookmarks", "users" add_foreign_key "contents", "lessons" add_foreign_key "flags", "project_submissions" add_foreign_key "flags", "users", column: "flagger_id" diff --git a/spec/factories/bookmarks.rb b/spec/factories/bookmarks.rb new file mode 100644 index 0000000000..70846d9207 --- /dev/null +++ b/spec/factories/bookmarks.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :bookmark do + user + lesson + end +end diff --git a/spec/models/bookmark_spec.rb b/spec/models/bookmark_spec.rb new file mode 100644 index 0000000000..1658451b98 --- /dev/null +++ b/spec/models/bookmark_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +RSpec.describe Bookmark do + subject(:bookmark) { create(:bookmark, lesson:) } + + let!(:lesson) { create(:lesson, section: create(:section, course:), course:) } + let!(:course) { create(:course) } + + describe 'associations' do + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:lesson) } + end + + describe 'validations' do + it 'validates uniqueness of lesson scoped to user' do + pending 'type error, course not getting attached to lesson' + expect(bookmark) + .to validate_uniqueness_of(:lesson) + .scoped_to(:user) + .with_message('user has already bookmarked this lesson') + end + end +end diff --git a/spec/models/lesson_spec.rb b/spec/models/lesson_spec.rb index 9ecb1e0cac..e49e442c41 100644 --- a/spec/models/lesson_spec.rb +++ b/spec/models/lesson_spec.rb @@ -9,6 +9,7 @@ it { is_expected.to have_many(:project_submissions) } it { is_expected.to have_many(:lesson_completions) } it { is_expected.to have_many(:completing_users).through(:lesson_completions) } + it { is_expected.to have_many(:bookmarks).dependent(:destroy) } it { is_expected.to validate_presence_of(:position) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 017851cf6f..750fd3a668 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -17,6 +17,8 @@ it { is_expected.to have_many(:notifications) } it { is_expected.to have_many(:likes).dependent(:destroy) } it { is_expected.to belong_to(:path).optional(true) } + it { is_expected.to have_many(:bookmarks).dependent(:destroy) } + it { is_expected.to have_many(:bookmarked_lessons).through(:bookmarks).source(:lesson) } context 'when user is created' do let!(:default_path) { create(:path, default_path: true) } From 7733e4f39e11070c7eb10f43b577b602441fa00a Mon Sep 17 00:00:00 2001 From: Dylan Will <58506115+ChargrilledChook@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:12:07 +1100 Subject: [PATCH 2/2] Fix error when navigating to a page without authentication --- app/controllers/lessons_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/lessons_controller.rb b/app/controllers/lessons_controller.rb index 032c36bd46..3e08c0c009 100644 --- a/app/controllers/lessons_controller.rb +++ b/app/controllers/lessons_controller.rb @@ -16,6 +16,7 @@ def set_lesson end def set_bookmark + return unless current_user return unless Feature.enabled?(:bookmarks, current_user) @bookmark = current_user.bookmarks.find_by(lesson_id: @lesson.id)