From e98fdeba0e1b06dda758d100f6ee97574a29ac48 Mon Sep 17 00:00:00 2001 From: rheniery Date: Tue, 24 Sep 2024 10:20:48 -0300 Subject: [PATCH] Add User Skills controller --- app/controllers/skills_controller.rb | 18 ++++ app/controllers/user_skills_controller.rb | 68 ++++++++++++++ app/models/skill.rb | 6 +- config/routes.rb | 8 +- ...3192947_add_unique_index_to_skills_name.rb | 7 ++ db/schema.rb | 4 +- .../user_skills_controller_spec.rb | 89 +++++++++++++++++++ spec/factories/skills.rb | 4 + spec/models/project_report_spec.rb | 4 +- spec/models/skill_spec.rb | 4 + 10 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 app/controllers/user_skills_controller.rb create mode 100644 db/migrate/20240923192947_add_unique_index_to_skills_name.rb create mode 100644 spec/controllers/user_skills_controller_spec.rb diff --git a/app/controllers/skills_controller.rb b/app/controllers/skills_controller.rb index a9c9d81..b162a15 100644 --- a/app/controllers/skills_controller.rb +++ b/app/controllers/skills_controller.rb @@ -4,4 +4,22 @@ class SkillsController < ApplicationController def index @skills = Skill.all end + + def create + @skill = Skill.new(skill_params) + + if @skill.save + render json: @skill, status: :created, location: @skill + else + render json: @skill.errors, status: :unprocessable_entity + end + end + + private + + + # Only allow a trusted parameter "white list" through. + def skill_params + params.require(:skill).permit(:name) + end end diff --git a/app/controllers/user_skills_controller.rb b/app/controllers/user_skills_controller.rb new file mode 100644 index 0000000..448cb1c --- /dev/null +++ b/app/controllers/user_skills_controller.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class UserSkillsController < ApplicationController + before_action :set_user_skill, only: %i[destroy] + + # GET /user_skills?user_id=:id + def index + @user_skills = UserSkill.where(user_id: params[:user_id]) + + render json: @user_skills + end + + # POST /user_skills + def create + @user_skill = UserSkill.new(user_skill_params) + + if @user_skill.save + render json: @user_skill, status: :created, location: @user_skill + else + render json: @user_skill.errors, status: :unprocessable_entity + end + end + + # PATCH/PUT /user_skills + def bulk_update + ApplicationRecord.transaction do + # Remove all existing skills for the user + UserSkill.where(user_id: params.dig(:params, :user_id)).destroy_all + + # Permit and process the new list of user skills + user_skills_list_params.each do |user_skill| + new_skill = UserSkill.new(user_skill.merge(user_id: params.dig(:params, :user_id))) + unless new_skill.save + raise ActiveRecord::Rollback + end + end + end + + render json: { message: 'Skills updated successfully' }, status: :ok + end + + # DELETE /user_skills/:id + def destroy + @user_skill.destroy + head :no_content + rescue ActiveRecord::RecordNotFound + head :not_found + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_user_skill + @user_skill = UserSkill.find(params[:id]) + end + + # Only allow a trusted parameter "white list" through. + def user_skills_list_params + params.require(:params).require(:user_skills).map do |skill| + skill.permit(:last_applied_in_year, :level, :years_of_experience, :skill_id) + end + end + + # Only allow a trusted parameter "white list" through. + def user_skill_params + params.require(:user_skill).permit(:last_applied_in_year, :level, :years_of_experience, :skill_id, :user_id) + end +end diff --git a/app/models/skill.rb b/app/models/skill.rb index 5065da1..d0b8448 100644 --- a/app/models/skill.rb +++ b/app/models/skill.rb @@ -9,9 +9,13 @@ # created_at :datetime not null # updated_at :datetime not null # +# Indexes +# +# index_skills_on_lower_name (lower((name)::text)) UNIQUE +# class Skill < ApplicationRecord has_many :user_skills, dependent: :destroy has_many :users, through: :user_skills - validates :name, presence: true + validates :name, presence: true, uniqueness: { case_sensitive: false } end diff --git a/config/routes.rb b/config/routes.rb index bf4cd36..e2e9b63 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,7 @@ require 'sidekiq/web' require 'sidekiq/cron/web' - +# rubocop:disable Metrics/BlockLength Rails.application.routes.draw do resources :dynamic_datasets resources :time_offs, only: [:create] @@ -38,4 +38,10 @@ resources :skills, only: [:index] resources :issues, only: [:index] resources :permissions, only: [:index] + resources :user_skills do + collection do + patch :bulk_update + end + end end +# rubocop:enable Metrics/BlockLength diff --git a/db/migrate/20240923192947_add_unique_index_to_skills_name.rb b/db/migrate/20240923192947_add_unique_index_to_skills_name.rb new file mode 100644 index 0000000..1a826c2 --- /dev/null +++ b/db/migrate/20240923192947_add_unique_index_to_skills_name.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddUniqueIndexToSkillsName < ActiveRecord::Migration[7.0] + def change + add_index :skills, 'LOWER(name)', unique: true, name: 'index_skills_on_lower_name' + end +end diff --git a/db/schema.rb b/db/schema.rb index 4258758..0b69d0b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2024_06_13_164100) do +ActiveRecord::Schema[7.0].define(version: 2024_09_23_192947) do # These are extensions that must be enabled in order to support this database - enable_extension "pg_stat_statements" enable_extension "plpgsql" enable_extension "unaccent" @@ -230,6 +229,7 @@ t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index "lower((name)::text)", name: "index_skills_on_lower_name", unique: true end create_table "statement_of_work_financial_reports", force: :cascade do |t| diff --git a/spec/controllers/user_skills_controller_spec.rb b/spec/controllers/user_skills_controller_spec.rb new file mode 100644 index 0000000..d116931 --- /dev/null +++ b/spec/controllers/user_skills_controller_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UserSkillsController, type: :controller do + let(:user) { double("User", id: 1) } + let(:skill) { double("Skill", id: 1, name: 'Ruby') } + let(:user_skill) { double("UserSkill", id: 1, user_id: user.id, skill_id: skill.id, years_of_experience: 3, last_applied_in_year: 2021, level: 'beginner') } + + before do + # Mocking UserSkill model interactions + allow(UserSkill).to receive(:where).and_return([user_skill]) + allow(UserSkill).to receive(:new).and_return(user_skill) + allow(user_skill).to receive(:save).and_return(true) + allow(user_skill).to receive(:destroy).and_return(true) + allow(UserSkill).to receive(:find).and_return(user_skill) + end + + describe 'GET #index' do + it 'returns a list of user skills' do + get :index, params: { user_id: user.id } + + expect(UserSkill).to have_received(:where).with(user_id: user.id) + expect(response).to have_http_status(:ok) + end + end + + describe 'POST #create' do + it 'creates a new user skill' do + post :create, params: { + user_skill: { + user_id: user.id, + skill_id: skill.id, + years_of_experience: 3, + last_applied_in_year: 2021, + level: 'beginner' + } + } + + expect(UserSkill).to have_received(:new).with( + 'user_id' => user.id.to_s, + 'skill_id' => skill.id.to_s, + 'years_of_experience' => '3', + 'last_applied_in_year' => '2021', + 'level' => 'beginner' + ) + expect(user_skill).to have_received(:save) + expect(response).to have_http_status(:created) + end + end + + describe 'PATCH #bulk_update' do + it 'updates the user skills' do + allow(UserSkill).to receive(:destroy_all).and_return(true) + patch :bulk_update, params: { + user_id: user.id, + user_skills: [ + { + last_applied_in_year: 2023, + level: 'intermediate', + years_of_experience: 2, + skill_id: skill.id + } + ] + } + + expect(UserSkill).to have_received(:destroy_all).with(user_id: user.id) + expect(UserSkill).to have_received(:new).with( + 'last_applied_in_year' => 2023, + 'level' => 'intermediate', + 'years_of_experience' => 2, + 'skill_id' => skill.id, + 'user_id' => user.id + ) + expect(user_skill).to have_received(:save) + expect(response).to have_http_status(:ok) + end + end + + describe 'DELETE #destroy' do + it 'deletes the user skill' do + delete :destroy, params: { id: user_skill.id } + + expect(UserSkill).to have_received(:find).with(user_skill.id.to_s) + expect(user_skill).to have_received(:destroy) + expect(response).to have_http_status(:no_content) + end + end +end diff --git a/spec/factories/skills.rb b/spec/factories/skills.rb index a6e462d..7eda4fd 100644 --- a/spec/factories/skills.rb +++ b/spec/factories/skills.rb @@ -9,6 +9,10 @@ # created_at :datetime not null # updated_at :datetime not null # +# Indexes +# +# index_skills_on_lower_name (lower((name)::text)) UNIQUE +# FactoryBot.define do factory :skill do name { FFaker::Skill.tech_skill } diff --git a/spec/models/project_report_spec.rb b/spec/models/project_report_spec.rb index e6b659b..84b9187 100644 --- a/spec/models/project_report_spec.rb +++ b/spec/models/project_report_spec.rb @@ -2,7 +2,7 @@ # == Schema Information # -# Table name: project_auths +# Table name: project_reports # # id :bigint not null, primary key # key :string @@ -12,7 +12,7 @@ # # Indexes # -# index_project_auths_on_project_id (project_id) +# index_project_reports_on_project_id (project_id) # # Foreign Keys # diff --git a/spec/models/skill_spec.rb b/spec/models/skill_spec.rb index ff87fe2..34a612a 100644 --- a/spec/models/skill_spec.rb +++ b/spec/models/skill_spec.rb @@ -9,6 +9,10 @@ # created_at :datetime not null # updated_at :datetime not null # +# Indexes +# +# index_skills_on_lower_name (lower((name)::text)) UNIQUE +# require 'rails_helper' RSpec.describe Skill, type: :model do