diff --git a/app/controllers/concerns/favorites_actions.rb b/app/controllers/concerns/favorites_actions.rb new file mode 100644 index 000000000..a056184ce --- /dev/null +++ b/app/controllers/concerns/favorites_actions.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module FavoritesActions + extend ActiveSupport::Concern + + included do + before_action :load_favorite_item, only: %i(favorite unfavorite) + end + + def favorite + @favorite_item.favorite!(current_user, current_team) + head :ok + end + + def unfavorite + @favorite_item.unfavorite!(current_user, current_team) + head :ok + end + + private + + def load_favorite_item + @favorite_item = controller_name.singularize.camelize.constantize.find(params[:id]) + + render_403 unless public_send(:"can_read_#{controller_name.singularize}?", @favorite_item) + end +end diff --git a/app/controllers/experiments_controller.rb b/app/controllers/experiments_controller.rb index a5b2172ab..280df07e8 100644 --- a/app/controllers/experiments_controller.rb +++ b/app/controllers/experiments_controller.rb @@ -7,6 +7,7 @@ class ExperimentsController < ApplicationController include ApplicationHelper include Rails.application.routes.url_helpers include Breadcrumbs + include FavoritesActions before_action :load_project, only: %i(index create archive_group restore_group move) before_action :load_experiment, except: %i(create archive_group restore_group diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index b1ba1783a..7a62fcb31 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -7,6 +7,7 @@ class MyModulesController < ApplicationController include ApplicationHelper include MyModulesHelper include Breadcrumbs + include FavoritesActions before_action :load_vars, except: %i(index restore_group create new save_table_state inventory_assigning_my_module_filter actions_toolbar) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e4651bc63..ee2b1a1db 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -9,6 +9,7 @@ class ProjectsController < ApplicationController include ExperimentsHelper include Breadcrumbs include UserRolesHelper + include FavoritesActions attr_reader :current_folder @@ -17,9 +18,10 @@ class ProjectsController < ApplicationController before_action :switch_team_with_param, only: :index before_action :load_vars, only: %i(update create_tag assigned_users_list) before_action :load_current_folder, only: :index - before_action :check_view_permissions, except: %i(index create update archive_group restore_group + before_action :check_read_permissions, except: %i(index create update archive_group restore_group inventory_assigning_project_filter - actions_toolbar user_roles users_filter head_of_project_users_list) + actions_toolbar user_roles users_filter head_of_project_users_list + favorite unfavorite) before_action :check_create_permissions, only: :create before_action :check_manage_permissions, only: :update before_action :set_folder_inline_name_editing, only: %i(index cards) @@ -338,7 +340,7 @@ class ProjectsController < ApplicationController end end - def check_view_permissions + def check_read_permissions current_team_switch(@project.team) if current_team != @project.team render_403 unless can_read_project?(@project) end diff --git a/app/models/concerns/favoritable.rb b/app/models/concerns/favoritable.rb new file mode 100644 index 000000000..9ffe6a8af --- /dev/null +++ b/app/models/concerns/favoritable.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Favoritable + extend ActiveSupport::Concern + + included do + has_many :favorites, as: :item, inverse_of: :item, dependent: :destroy + + scope :favorite_for, ->(user) { joins(:favorites).where(favorites: { user: user }) } + end + + def favorite!(user, favorite_team = nil) + favorites.create!(user: user, team: favorite_team || team) + end + + def unfavorite!(user, favorite_team = nil) + favorites.find_by(user: user, team: favorite_team || team).destroy! + end +end diff --git a/app/models/experiment.rb b/app/models/experiment.rb index c5c3c6f20..7cae21930 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -14,6 +14,7 @@ class Experiment < ApplicationRecord include Assignable include Cloneable include TimeTrackable + include Favoritable before_save -> { report_elements.destroy_all }, if: -> { !new_record? && project_id_changed? } before_save :reset_due_date_notification_sent, if: -> { due_date_changed? } diff --git a/app/models/favorite.rb b/app/models/favorite.rb new file mode 100644 index 000000000..c5a52cdd4 --- /dev/null +++ b/app/models/favorite.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Favorite < ApplicationRecord + belongs_to :user + belongs_to :team + belongs_to :item, polymorphic: true +end diff --git a/app/models/my_module.rb b/app/models/my_module.rb index 177768b14..f6fa95e14 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -13,6 +13,7 @@ class MyModule < ApplicationRecord include PermissionCheckableModel include Assignable include Cloneable + include Favoritable attr_accessor :transition_error_rollback, :my_module_status_created_by diff --git a/app/models/project.rb b/app/models/project.rb index e6a8f4908..c75bdcb64 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -12,6 +12,7 @@ class Project < ApplicationRecord include PermissionCheckableModel include Assignable include TimeTrackable + include Favoritable enum visibility: { hidden: 0, visible: 1 } diff --git a/app/models/user.rb b/app/models/user.rb index 764866196..73b2c8a54 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -327,6 +327,8 @@ class User < ApplicationRecord has_many :hidden_repository_cell_reminders, dependent: :destroy + has_many :favorites, dependent: :destroy + before_validation :downcase_email! def name diff --git a/app/serializers/lists/experiment_serializer.rb b/app/serializers/lists/experiment_serializer.rb index b8d823799..9e033066e 100644 --- a/app/serializers/lists/experiment_serializer.rb +++ b/app/serializers/lists/experiment_serializer.rb @@ -68,7 +68,9 @@ module Lists clone: clone_experiment_path(object), update: experiment_path(object), show_access: access_permissions_experiment_path(object), - workflow_img: fetch_workflow_img_experiment_path(object) + workflow_img: fetch_workflow_img_experiment_path(object), + favorite: favorite_experiment_url(object), + unfavorite: unfavorite_experiment_url(object) } if can_manage_project_users?(object.project) diff --git a/app/serializers/lists/my_module_serializer.rb b/app/serializers/lists/my_module_serializer.rb index 4a8540630..3128f7ffe 100644 --- a/app/serializers/lists/my_module_serializer.rb +++ b/app/serializers/lists/my_module_serializer.rb @@ -66,7 +66,9 @@ module Lists experiments_to_move: experiments_to_move_experiment_path(object.experiment), update: my_module_path(object), show_access: access_permissions_my_module_path(object), - provisioning_status: provisioning_status_my_module_url(object) + provisioning_status: provisioning_status_my_module_url(object), + favorite: favorite_my_module_url(object), + unfavorite: unfavorite_my_module_url(object) } urls_list[:update_access] = access_permissions_my_module_path(object) if can_manage_project_users?(object.experiment.project) diff --git a/app/serializers/lists/project_and_folder_serializer.rb b/app/serializers/lists/project_and_folder_serializer.rb index b5d0fe34f..65877876c 100644 --- a/app/serializers/lists/project_and_folder_serializer.rb +++ b/app/serializers/lists/project_and_folder_serializer.rb @@ -126,6 +126,11 @@ module Lists project_folder_path(object) end + if project? + urls_list[:favorite] = favorite_project_url(object) + urls_list[:unfavorite] = unfavorite_project_url(object) + end + urls_list[:show_access] = access_permissions_project_path(object) if project? && can_manage_project_users?(object) urls_list[:assigned_users] = assigned_users_list_project_path(object) diff --git a/config/routes.rb b/config/routes.rb index 46f905d70..1bcc70cfd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -389,6 +389,8 @@ Rails.application.routes.draw do member do get :assigned_users_list + post :favorite + post :unfavorite end collection do @@ -457,6 +459,8 @@ Rails.application.routes.draw do get :projects_to_clone get :projects_to_move get :experiments_to_move + post :favorite + post :unfavorite end end @@ -476,6 +480,8 @@ Rails.application.routes.draw do get :permissions get :actions_dropdown get :provisioning_status + post :favorite + post :unfavorite end resources :my_module_tags, path: '/tags', only: [:index, :create, :destroy] do collection do diff --git a/db/migrate/20250428070801_create_favorites.rb b/db/migrate/20250428070801_create_favorites.rb new file mode 100644 index 000000000..58587f447 --- /dev/null +++ b/db/migrate/20250428070801_create_favorites.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class CreateFavorites < ActiveRecord::Migration[7.0] + def change + create_table :favorites do |t| + t.references :user, null: false, foreign_key: true + t.references :team, null: false, foreign_key: true + t.references :item, polymorphic: true, null: false + + t.timestamps + end + + add_index( + :favorites, + %i(user_id team_id item_id item_type), + unique: true, + name: :index_favorites_on_user_and_item_and_team + ) + end +end diff --git a/spec/controllers/experiments_controller_spec.rb b/spec/controllers/experiments_controller_spec.rb index 9fcf44a0e..350081a09 100644 --- a/spec/controllers/experiments_controller_spec.rb +++ b/spec/controllers/experiments_controller_spec.rb @@ -103,4 +103,24 @@ describe ExperimentsController, type: :controller do .to(change { Activity.count }) end end + + describe 'POST favorite' do + let(:action) { post :favorite, params: { id: experiment.id } } + + it 'creates a favorite' do + expect(user.favorites.exists?(item: experiment )).to eq(false) + action + expect(user.favorites.exists?(item: experiment )).to eq(true) + end + end + + describe 'POST unfavorite' do + let(:action) { post :unfavorite, params: { id: experiment.id } } + + it 'removes a favorite' do + Favorite.create!(user: user, item: experiment, team: experiment.team) + action + expect(user.favorites.exists?(item: experiment )).to eq(false) + end + end end diff --git a/spec/controllers/my_modules_controller_spec.rb b/spec/controllers/my_modules_controller_spec.rb index 42de9242f..f4df88d10 100644 --- a/spec/controllers/my_modules_controller_spec.rb +++ b/spec/controllers/my_modules_controller_spec.rb @@ -256,4 +256,24 @@ describe MyModulesController, type: :controller do end end end + + describe 'POST favorite' do + let(:action) { post :favorite, params: { id: my_module.id } } + + it 'creates a favorite' do + expect(user.favorites.exists?(item: my_module )).to eq(false) + action + expect(user.favorites.exists?(item: my_module )).to eq(true) + end + end + + describe 'POST unfavorite' do + let(:action) { post :unfavorite, params: { id: my_module.id } } + + it 'removes a favorite' do + Favorite.create!(user: user, item: my_module, team: my_module.team) + action + expect(user.favorites.exists?(item: my_module )).to eq(false) + end + end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 704fe824d..74d76acb2 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -83,4 +83,24 @@ describe ProjectsController, type: :controller do end end end + + describe 'POST favorite' do + let(:action) { post :favorite, params: { id: project.id } } + + it 'creates a favorite' do + expect(user.favorites.exists?(item: project )).to eq(false) + action + expect(user.favorites.exists?(item: project )).to eq(true) + end + end + + describe 'POST unfavorite' do + let(:action) { post :unfavorite, params: { id: project.id } } + + it 'removes a favorite' do + Favorite.create!(user: user, item: project, team: project.team) + action + expect(user.favorites.exists?(item: project )).to eq(false) + end + end end