From 90b01b0883697d3f7e08b298c4bd36479af0d9d1 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 11 Apr 2023 23:49:25 -0400 Subject: [PATCH 01/30] add invitation model and invitation model tests Followed test driven development to implement invitation model. Added FactoryBot to Gemfile. --- Gemfile | 1 + app/models/assignment.rb | 1 + app/models/invitation.rb | 15 +++++++ app/models/user.rb | 1 + .../20230412020156_create_invitations.rb | 15 +++++++ db/schema.rb | 28 +++++++++---- spec/factories.rb | 18 ++++++++ spec/models/invitation_spec.rb | 41 +++++++++++++++++++ spec/rails_helper.rb | 2 +- spec/support/factory_bot.rb | 3 ++ 10 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 app/models/invitation.rb create mode 100644 db/migrate/20230412020156_create_invitations.rb create mode 100644 spec/factories.rb create mode 100644 spec/models/invitation_spec.rb create mode 100644 spec/support/factory_bot.rb diff --git a/Gemfile b/Gemfile index 8e571b54..ac9f1970 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,7 @@ group :development, :test do gem 'simplecov', require: false, group: :test gem 'rspec-rails' gem 'rswag-specs' + gem 'factory_bot_rails' end group :development do diff --git a/app/models/assignment.rb b/app/models/assignment.rb index f4e270dc..f286d568 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -1,2 +1,3 @@ class Assignment < ApplicationRecord + has_many :invitations end diff --git a/app/models/invitation.rb b/app/models/invitation.rb new file mode 100644 index 00000000..e672c600 --- /dev/null +++ b/app/models/invitation.rb @@ -0,0 +1,15 @@ +class Invitation < ApplicationRecord + belongs_to :to_user, class_name: 'User', foreign_key: 'to_id', inverse_of: false + belongs_to :from_user, class_name: 'User', foreign_key: 'from_id', inverse_of: false + belongs_to :assignment, class_name: 'Assignment', foreign_key: 'assignment_id' + validates :reply_status, presence: true, length: { maximum: 1 } + validates_inclusion_of :reply_status, in: %w[W A R], allow_nil: false + validate :to_from_cant_be_same + + # validate if the to_id and from_id are same + def to_from_cant_be_same + if self.from_id == self.to_id + errors.add(:from_id, "to and from users should be different") + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index da032335..4b4f000d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -13,6 +13,7 @@ class User < ApplicationRecord belongs_to :institution, optional: true belongs_to :parent, class_name: 'User', optional: true has_many :users, foreign_key: 'parent_id', dependent: :nullify + has_many :invitations scope :students, -> { where role_id: Role::STUDENT } scope :tas, -> { where role_id: Role::TEACHING_ASSISTANT } diff --git a/db/migrate/20230412020156_create_invitations.rb b/db/migrate/20230412020156_create_invitations.rb new file mode 100644 index 00000000..7f3f3c05 --- /dev/null +++ b/db/migrate/20230412020156_create_invitations.rb @@ -0,0 +1,15 @@ +class CreateInvitations < ActiveRecord::Migration[7.0] + def change + create_table :invitations do |t| + t.integer "assignment_id" + t.integer "from_id" + t.integer "to_id" + t.string "reply_status", limit: 1 + t.index ["assignment_id"], name: "fk_invitation_assignments" + t.index ["from_id"], name: "fk_invitationfrom_users" + t.index ["to_id"], name: "fk_invitationto_users" + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 7af1bf3e..fc19ab89 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_03_15_185139) do +ActiveRecord::Schema[7.0].define(version: 2023_04_12_020156) do create_table "assignments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name" t.string "directory_path" @@ -71,6 +71,18 @@ t.datetime "updated_at", null: false end + create_table "invitations", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| + t.integer "assignment_id" + t.integer "from_id" + t.integer "to_id" + t.string "reply_status", limit: 1 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["assignment_id"], name: "fk_invitation_assignments" + t.index ["from_id"], name: "fk_invitationfrom_users" + t.index ["to_id"], name: "fk_invitationto_users" + end + create_table "roles", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name" t.bigint "parent_id" @@ -88,17 +100,17 @@ t.string "email" t.integer "parent_id" t.string "mru_directory_path" - t.boolean "email_on_review" - t.boolean "email_on_submission" - t.boolean "email_on_review_of_review" - t.boolean "is_new_user" - t.boolean "master_permission_granted" + t.boolean "email_on_review", default: false + t.boolean "email_on_submission", default: false + t.boolean "email_on_review_of_review", default: false + t.boolean "is_new_user", default: true + t.boolean "master_permission_granted", default: false t.string "handle" t.string "persistence_token" t.string "timezonepref" - t.boolean "copy_of_emails" + t.boolean "copy_of_emails", default: false t.integer "institution_id" - t.boolean "etc_icons_on_homepage" + t.boolean "etc_icons_on_homepage", default: false t.integer "locale" t.datetime "created_at", null: false t.datetime "updated_at", null: false diff --git a/spec/factories.rb b/spec/factories.rb new file mode 100644 index 00000000..eda3b9db --- /dev/null +++ b/spec/factories.rb @@ -0,0 +1,18 @@ +FactoryBot.define do + + factory :user do + sequence(:name) { |n| n = n % 3; "student206#{n + 4}" } + email { "joe@gmail.com" } + password { "blahblahblah" } + sequence(:fullname) { |n| n = n % 3; "206#{n + 4}, student" } + role factory: :role + end + + factory :role do + name { "Student" } + end + + factory :assignment do + name { (Assignment.last ? ('assignment' + (Assignment.last.id + 1).to_s) : 'final2').to_s } + end +end \ No newline at end of file diff --git a/spec/models/invitation_spec.rb b/spec/models/invitation_spec.rb new file mode 100644 index 00000000..4a1802ea --- /dev/null +++ b/spec/models/invitation_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +RSpec.describe Invitation, type: :model do + let(:user1) { build(:user, id: 4, name: 'no name', fullname: 'no two') } + let(:user2) { build(:user, id: 5, name: 'no name 2', fullname: 'no two 2') } + let(:assignment) { build(:assignment)} + + after(:each) do + ActionMailer::Base.deliveries.clear + end + + it "is valid with valid attributes" do + invitation = Invitation.new(to_user: user1, from_user: user2, assignment: assignment, reply_status: 'W') + expect(invitation).to be_valid + end + + it "is invalid with same from and to attribute" do + invitation = Invitation.new(to_user: user1, from_user: user1, assignment: assignment, reply_status: 'W') + expect(invitation).to_not be_valid + end + + it "is invalid with invalid to user attribute" do + invitation = Invitation.new(to_user: nil, from_user: user2, assignment: assignment, reply_status: 'W') + expect(invitation).to_not be_valid + end + + it "is invalid with invalid from user attribute" do + invitation = Invitation.new(to_user: user1, from_user: nil, assignment: assignment, reply_status: 'W') + expect(invitation).to_not be_valid + end + + it "is invalid with invalid assignment attribute" do + invitation = Invitation.new(to_user: user1, from_user: user2, assignment: nil, reply_status: 'W') + expect(invitation).to_not be_valid + end + + it "is invalid with invalid reply_status attribute" do + invitation = Invitation.new(to_user: user1, from_user: user2, assignment: assignment, reply_status: 'X') + expect(invitation).to_not be_valid + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index a53bdba2..922f6458 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -6,7 +6,7 @@ abort("The Rails environment is running in production mode!") if Rails.env.production? require 'rspec/rails' # Add additional requires below this line. Rails is not loaded until this point! - +require 'support/factory_bot' # Requires supporting ruby files with custom matchers and macros, etc, in # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are # run as spec files by default. This means that files in spec/support that end diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100644 index 00000000..195f20aa --- /dev/null +++ b/spec/support/factory_bot.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end \ No newline at end of file From f01418aaacf5e760cee8f58c965457f1ed14bbf8 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Thu, 13 Apr 2023 12:29:04 -0400 Subject: [PATCH 02/30] Update Gemfile.lock --- Gemfile.lock | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 428c0f22..472d6388 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -80,6 +80,11 @@ GEM diff-lcs (1.5.0) docile (1.4.0) erubi (1.12.0) + factory_bot (6.2.1) + activesupport (>= 5.0.0) + factory_bot_rails (6.2.0) + factory_bot (~> 6.2.0) + railties (>= 5.0.0) globalid (1.1.0) activesupport (>= 5.0) i18n (1.12.0) @@ -111,6 +116,8 @@ GEM net-smtp (0.3.3) net-protocol nio4r (2.5.8) + nokogiri (1.14.2-x86_64-darwin) + racc (~> 1.4) nokogiri (1.14.2-x86_64-linux) racc (~> 1.4) parallel (1.22.1) @@ -213,12 +220,14 @@ GEM zeitwerk (2.6.7) PLATFORMS + x86_64-darwin-22 x86_64-linux DEPENDENCIES bcrypt (~> 3.1.7) bootsnap debug + factory_bot_rails mysql2 (~> 0.5.5) puma (~> 5.0) rack-cors From e70a4d3bce8057de1c5a4623a04cd68a3be81ffe Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Thu, 13 Apr 2023 15:56:21 -0400 Subject: [PATCH 03/30] add method signatures Added method signatures that will act as placeholders for further implementation. --- .../api/v1/invitations_controller.rb | 46 +++++++++++++++++++ app/models/invitation.rb | 36 +++++++++++++++ config/routes.rb | 3 ++ 3 files changed, 85 insertions(+) create mode 100644 app/controllers/api/v1/invitations_controller.rb diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb new file mode 100644 index 00000000..b890029c --- /dev/null +++ b/app/controllers/api/v1/invitations_controller.rb @@ -0,0 +1,46 @@ +class Api::V1::InvitationsController < ApplicationController + rescue_from ActiveRecord::RecordNotFound, with: :invite_not_found + + # GET /api/v1/invitations + def index; end + + # POST /api/v1/invitations/ + def create; end + + # GET /api/v1/invitations/:id + def show; end + + # PATCH /api/v1/invitations/:id + def update; end + + # DELETE /api/v1/invitations/:id + def delete; end + + # GET /invitations/:user_id/:assignment_id + def list_all_invitations_for_user_assignment; end + + private + + # This method will check if the invited user exists. + def check_invited_user_before_invitation; end + + # This method will check if the invited user is a participant in the assignment. + def check_participant_before_invitation; end + + # This method will check if the team meets the joining requirement before sending an invite. + # NOTE: This method depends on TeamUser and AssignmentTeam, which is not implemented yet. + def check_team_before_invitation; end + + # This method will check if the team meets the joining requirements + # when an invitation is being accepted + # NOTE: This method depends on AssignmentParticipant and AssignmentTeam + # which is not implemented yet. + def check_team_before_accept; end + + # only allow a list of valid invite params + def invite_params; end + + # helper method used when invite is not found + def invite_not_found; end + +end diff --git a/app/models/invitation.rb b/app/models/invitation.rb index e672c600..2e5c07f6 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -12,4 +12,40 @@ def to_from_cant_be_same errors.add(:from_id, "to and from users should be different") end end + + # Return a new invitation + # params = :assignment_id, :to_id, :from_id, :reply_status + def invitation_factory(params); end + + # send invite email + def send_invite_email; end + + # Remove all invites sent by a user for an assignment. + def self.remove_users_sent_invites_for_assignment(user_id, assignment_id); end + + # After a users accepts an invite, the teams_users table needs to be updated. + # NOTE: Depends on TeamUser model, which is not implemented yet. + def self.update_users_topic_after_invite_accept(inviter_user_id, invited_user_id, assignment_id); end + + # This method handles all that needs to be done upon a user accepting an invitation. + # First the users previous team is deleted if they were the only member of that + # team and topics that the old team signed up for will be deleted. + # Then invites the user that accepted the invite sent will be removed. + # Last the users team entry will be added to the TeamsUser table and their assigned topic is updated + def self.accept_invitation(invite_id, logged_in_user); end + + # This method handles all that needs to be done upon an user decline an invitation. + def self.decline_invitation(invite_id, logged_in_user); end + + # This method handles all that need to be done upon an invitation retraction. + def self.retract_invitation(invite_id, logged_in_user); end + + # check if the user is invited + def self.invited?(invitee_user_id, invited_user_id, assignment_id); end + + # This will override the default as_json method in the ApplicationRecord class and specify + def as_json(options = {}); end + + def set_defaults; end + end diff --git a/config/routes.rb b/config/routes.rb index 3a27d696..33e5ee74 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -14,6 +14,9 @@ get ':id/managed', on: :collection, action: :managed_users end resources :assignments + resources :invitations do + get '/:user_id/:assignment_id/', on: :collection, action: :list_all_invitations_for_user_assignment + end end end end From c7256fa9def42b754a30b153229df8c6e516d874 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 18 Apr 2023 11:54:38 -0400 Subject: [PATCH 04/30] implement index route with TDD --- .../api/v1/invitations_controller.rb | 5 +++- app/models/invitation.rb | 12 +++++++++- .../api/v1/invitation_controller_spec.rb | 24 +++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 spec/requests/api/v1/invitation_controller_spec.rb diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index b890029c..ffaa76c3 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -2,7 +2,10 @@ class Api::V1::InvitationsController < ApplicationController rescue_from ActiveRecord::RecordNotFound, with: :invite_not_found # GET /api/v1/invitations - def index; end + def index + @invitations = Invitation.all + render json: @invitations + end # POST /api/v1/invitations/ def create; end diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 2e5c07f6..b458a9fa 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -44,7 +44,17 @@ def self.retract_invitation(invite_id, logged_in_user); end def self.invited?(invitee_user_id, invited_user_id, assignment_id); end # This will override the default as_json method in the ApplicationRecord class and specify - def as_json(options = {}); end + def as_json(options = {}) + super(options.merge({ + only: %i[id reply_status created_at updated_at], + include: { + assignment: { only: %i[id name] }, + from_user: { only: %i[id name fullname email] }, + to_user: { only: %i[id name fullname email] } + } + })).tap do |hash| + end + end def set_defaults; end diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb new file mode 100644 index 00000000..3c1f25b3 --- /dev/null +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -0,0 +1,24 @@ +require 'swagger_helper' + +RSpec.describe 'Roles API', type: :request do + + path '/api/v1/invitations' do + get('list invitations') do + tags 'Invitations' + produces 'application/json' + + response(200, 'successful') do + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + + run_test! + end + end + end +end From 942665b7a2151c4690439599221e2ec18ddcb668 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 18 Apr 2023 13:16:57 -0400 Subject: [PATCH 05/30] implement InvitationController#create using TDD implemented InvitationController#create update invitation.rb add validation to prevent duplicate invitation. add faker in Gemfile --- Gemfile | 1 + Gemfile.lock | 3 ++ .../api/v1/invitations_controller.rb | 15 ++++++-- app/models/invitation.rb | 6 ++- spec/factories.rb | 8 ++-- .../api/v1/invitation_controller_spec.rb | 37 ++++++++++++++++++- 6 files changed, 61 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index ac9f1970..f57589e6 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,7 @@ group :development, :test do gem 'rspec-rails' gem 'rswag-specs' gem 'factory_bot_rails' + gem 'faker' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 472d6388..1f1a7c45 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -85,6 +85,8 @@ GEM factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) + faker (3.2.0) + i18n (>= 1.8.11, < 2) globalid (1.1.0) activesupport (>= 5.0) i18n (1.12.0) @@ -228,6 +230,7 @@ DEPENDENCIES bootsnap debug factory_bot_rails + faker mysql2 (~> 0.5.5) puma (~> 5.0) rack-cors diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index ffaa76c3..7308ed06 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -8,7 +8,15 @@ def index end # POST /api/v1/invitations/ - def create; end + def create + params[:invitation][:reply_status] ||= 'W' + @invitation = Invitation.new(invite_params) + if @invitation.save + render json: @invitation, status: :created + else + render json: @invitation.errors, status: :unprocessable_entity + end + end # GET /api/v1/invitations/:id def show; end @@ -41,8 +49,9 @@ def check_team_before_invitation; end def check_team_before_accept; end # only allow a list of valid invite params - def invite_params; end - + def invite_params + params.require(:invitation).permit(:id, :assignment_id, :from_id, :to_id, :reply_status) + end # helper method used when invite is not found def invite_not_found; end diff --git a/app/models/invitation.rb b/app/models/invitation.rb index b458a9fa..25f31db9 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -4,12 +4,16 @@ class Invitation < ApplicationRecord belongs_to :assignment, class_name: 'Assignment', foreign_key: 'assignment_id' validates :reply_status, presence: true, length: { maximum: 1 } validates_inclusion_of :reply_status, in: %w[W A R], allow_nil: false + validates :assignment_id, uniqueness: { + scope: %i[from_id to_id reply_status], + message: 'You cannot have duplicate invitations' + } validate :to_from_cant_be_same # validate if the to_id and from_id are same def to_from_cant_be_same if self.from_id == self.to_id - errors.add(:from_id, "to and from users should be different") + errors.add(:from_id, 'to and from users should be different') end end diff --git a/spec/factories.rb b/spec/factories.rb index eda3b9db..461e2a03 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,15 +1,15 @@ FactoryBot.define do factory :user do - sequence(:name) { |n| n = n % 3; "student206#{n + 4}" } - email { "joe@gmail.com" } + sequence(:name) { |n| "#{Faker::Name.name}".delete(" \t\r\n").downcase } + sequence(:email) { |n| "#{Faker::Internet.email}"} password { "blahblahblah" } - sequence(:fullname) { |n| n = n % 3; "206#{n + 4}, student" } + sequence(:fullname) { |n| "#{Faker::Name.name}#{Faker::Name.name}".downcase } role factory: :role end factory :role do - name { "Student" } + name { Faker::Name.name} end factory :assignment do diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index 3c1f25b3..4d37f080 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -1,8 +1,13 @@ require 'swagger_helper' +require 'rails_helper' -RSpec.describe 'Roles API', type: :request do +RSpec.describe 'Invitations API', type: :request do + let(:user1) { create :user } + let(:user2) { create :user } + let(:assignment) { create(:assignment) } path '/api/v1/invitations' do + get('list invitations') do tags 'Invitations' produces 'application/json' @@ -20,5 +25,35 @@ run_test! end end + + post('create invitation') do + tags 'Invitations' + consumes 'application/json' + parameter name: :invitation, in: :body, schema: { + type: :object, + properties: { + assignment_id: { type: :integer }, + from_id: { type: :integer }, + to_id: { type: :integer }, + reply_status: { type: :string } + }, + required: %w[assignment_id from_id to_id] + } + + response(201, 'Create an invitation') do + let(:invitation) { {to_id: user1.id, from_id: user2.id, assignment_id: assignment.id} } + after do |example| + p example + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + + end + end end From 3ed545061de22b77d92642e1c7f0cc69f8d18245 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 18 Apr 2023 15:11:17 -0400 Subject: [PATCH 06/30] add more tests for InvitationController --- .../api/v1/invitation_controller_spec.rb | 69 +++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index 4d37f080..71f10e25 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -2,8 +2,9 @@ require 'rails_helper' RSpec.describe 'Invitations API', type: :request do - let(:user1) { create :user } - let(:user2) { create :user } + let(:user1) { create :user, name: "rohitgeddam" } + let(:user2) { create :user, name: "superman" } + let(:invalid_user) { build :user, name: "INVALID"} let(:assignment) { create(:assignment) } path '/api/v1/invitations' do @@ -40,10 +41,9 @@ required: %w[assignment_id from_id to_id] } - response(201, 'Create an invitation') do + response(201, 'Create an invitation with valid parameters') do let(:invitation) { {to_id: user1.id, from_id: user2.id, assignment_id: assignment.id} } after do |example| - p example example.metadata[:response][:content] = { 'application/json' => { example: JSON.parse(response.body, symbolize_names: true) @@ -53,6 +53,67 @@ run_test! end + response(422, 'Create an invitation with invalid to user parameters') do + let(:invitation) { {to_id: invalid_user.id, from_id: user2.id, assignment_id: assignment.id} } + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + + response(422, 'Create an invitation with invalid from user parameters') do + let(:invitation) { {to_id: user1.id, from_id: invalid_user.id, assignment_id: assignment.id} } + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + + response(422, 'Create an invitation with invalid assignment parameters') do + let(:invitation) { {to_id: user1.id, from_id: user2.id, assignment_id: nil} } + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + + response(422, 'Create an invitation with invalid reply_status parameters') do + let(:invitation) { { to_id: user1.id, from_id: user2.id, assignment_id: assignment.id, reply_status: "I" } } + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + + response(422, 'Create an invitation with same to user and from user parameters') do + let(:invitation) { { to_id: user1.id, from_id: user1.id, assignment_id: assignment.id } } + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + + end end From 42c4b938612113c549ee56f11484907319470230 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 18 Apr 2023 15:11:29 -0400 Subject: [PATCH 07/30] add Invitation factory --- spec/factories.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/spec/factories.rb b/spec/factories.rb index 461e2a03..cac30aad 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -15,4 +15,11 @@ factory :assignment do name { (Assignment.last ? ('assignment' + (Assignment.last.id + 1).to_s) : 'final2').to_s } end + + factory :invitation do + from_user factory: :user + to_user factory: :user + assignment factory: :assignment + reply_status { "W" } + end end \ No newline at end of file From 8695e57e7d2f5e3ff6a2d48f0be5429a598156c2 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 18 Apr 2023 15:26:52 -0400 Subject: [PATCH 08/30] implement InvitationController#show using TDD --- .../api/v1/invitations_controller.rb | 12 +++++- .../api/v1/invitation_controller_spec.rb | 40 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index 7308ed06..086a18fa 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -19,7 +19,12 @@ def create end # GET /api/v1/invitations/:id - def show; end + def show + @invitation = Invitation.find(params[:id]) + render json: @invitation, status: :ok + rescue ActiveRecord::RecordNotFound => e + render json: { error: e.message }, status: :not_found + end # PATCH /api/v1/invitations/:id def update; end @@ -52,7 +57,10 @@ def check_team_before_accept; end def invite_params params.require(:invitation).permit(:id, :assignment_id, :from_id, :to_id, :reply_status) end + # helper method used when invite is not found - def invite_not_found; end + def invite_not_found + render json: { error: "Invitation with id #{params[:id]} not found" }, status: :not_found + end end diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index 71f10e25..1a8c00cb 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -5,6 +5,7 @@ let(:user1) { create :user, name: "rohitgeddam" } let(:user2) { create :user, name: "superman" } let(:invalid_user) { build :user, name: "INVALID"} + let(:invitation) { create :invitation, from_user: user1, to_user: user2, assignment: assignment} let(:assignment) { create(:assignment) } path '/api/v1/invitations' do @@ -117,4 +118,43 @@ end end + + + path '/api/v1/invitations/{id}' do + parameter name: 'id', in: :path, type: :integer, description: 'id of the invitation' + + get('show invitation') do + tags 'Invitations' + response(200, 'show request with valid invitation id') do + let(:id) { invitation.id } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + + end + + get('show invitation') do + tags 'Invitations' + response(404, 'show request with invalid invitation id') do + let(:id) { "INVALID" } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end + end From e48982c90e1e12d442481d220cb58eaae65ee05b Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 18 Apr 2023 20:38:27 -0400 Subject: [PATCH 09/30] implement InvitationController#update using TDD --- .../api/v1/invitations_controller.rb | 18 ++++- app/models/invitation.rb | 13 +++- .../api/v1/invitation_controller_spec.rb | 73 ++++++++++++++++--- 3 files changed, 88 insertions(+), 16 deletions(-) diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index 086a18fa..df446397 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -9,7 +9,7 @@ def index # POST /api/v1/invitations/ def create - params[:invitation][:reply_status] ||= 'W' + params[:invitation][:reply_status] ||= Invitation::WAITING_STATUS @invitation = Invitation.new(invite_params) if @invitation.save render json: @invitation, status: :created @@ -27,7 +27,21 @@ def show end # PATCH /api/v1/invitations/:id - def update; end + def update + @invite_id = params[:id] + @invitation = Invitation.find(@invite_id) + case params[:reply_status] + when Invitation::ACCEPT_STATUS + Invitation.accept_invitation(@invitation, nil) + render json: @invitation, status: :ok + when Invitation::REJECT_STATUS + Invitation.decline_invitation(@invitation, nil) + render json: @invitation, status: :ok + else + render json: @invitation.errors, status: :unprocessable_entity + end + + end # DELETE /api/v1/invitations/:id def delete; end diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 25f31db9..cdb21a0c 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -1,9 +1,12 @@ class Invitation < ApplicationRecord + ACCEPT_STATUS = 'A' + REJECT_STATUS = 'R' + WAITING_STATUS = 'W' belongs_to :to_user, class_name: 'User', foreign_key: 'to_id', inverse_of: false belongs_to :from_user, class_name: 'User', foreign_key: 'from_id', inverse_of: false belongs_to :assignment, class_name: 'Assignment', foreign_key: 'assignment_id' validates :reply_status, presence: true, length: { maximum: 1 } - validates_inclusion_of :reply_status, in: %w[W A R], allow_nil: false + validates_inclusion_of :reply_status, in: [ACCEPT_STATUS, REJECT_STATUS, WAITING_STATUS], allow_nil: false validates :assignment_id, uniqueness: { scope: %i[from_id to_id reply_status], message: 'You cannot have duplicate invitations' @@ -36,10 +39,14 @@ def self.update_users_topic_after_invite_accept(inviter_user_id, invited_user_id # team and topics that the old team signed up for will be deleted. # Then invites the user that accepted the invite sent will be removed. # Last the users team entry will be added to the TeamsUser table and their assigned topic is updated - def self.accept_invitation(invite_id, logged_in_user); end + def self.accept_invitation(invitation, logged_in_user) + invitation.update(reply_status: ACCEPT_STATUS) + end # This method handles all that needs to be done upon an user decline an invitation. - def self.decline_invitation(invite_id, logged_in_user); end + def self.decline_invitation(invitation, logged_in_user) + invitation.update(reply_status: REJECT_STATUS) + end # This method handles all that need to be done upon an invitation retraction. def self.retract_invitation(invite_id, logged_in_user); end diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index 1a8c00cb..14eea9c6 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -2,11 +2,11 @@ require 'rails_helper' RSpec.describe 'Invitations API', type: :request do - let(:user1) { create :user, name: "rohitgeddam" } - let(:user2) { create :user, name: "superman" } - let(:invalid_user) { build :user, name: "INVALID"} - let(:invitation) { create :invitation, from_user: user1, to_user: user2, assignment: assignment} + let(:user1) { create :user, name: 'rohitgeddam' } + let(:user2) { create :user, name: 'superman' } + let(:invalid_user) { build :user, name: 'INVALID' } let(:assignment) { create(:assignment) } + let(:invitation) { create :invitation, from_user: user1, to_user: user2, assignment: assignment } path '/api/v1/invitations' do @@ -43,7 +43,7 @@ } response(201, 'Create an invitation with valid parameters') do - let(:invitation) { {to_id: user1.id, from_id: user2.id, assignment_id: assignment.id} } + let(:invitation) { { to_id: user1.id, from_id: user2.id, assignment_id: assignment.id } } after do |example| example.metadata[:response][:content] = { 'application/json' => { @@ -55,7 +55,7 @@ end response(422, 'Create an invitation with invalid to user parameters') do - let(:invitation) { {to_id: invalid_user.id, from_id: user2.id, assignment_id: assignment.id} } + let(:invitation) { { to_id: invalid_user.id, from_id: user2.id, assignment_id: assignment.id } } after do |example| example.metadata[:response][:content] = { 'application/json' => { @@ -67,7 +67,7 @@ end response(422, 'Create an invitation with invalid from user parameters') do - let(:invitation) { {to_id: user1.id, from_id: invalid_user.id, assignment_id: assignment.id} } + let(:invitation) { { to_id: user1.id, from_id: invalid_user.id, assignment_id: assignment.id } } after do |example| example.metadata[:response][:content] = { 'application/json' => { @@ -79,7 +79,7 @@ end response(422, 'Create an invitation with invalid assignment parameters') do - let(:invitation) { {to_id: user1.id, from_id: user2.id, assignment_id: nil} } + let(:invitation) { { to_id: user1.id, from_id: user2.id, assignment_id: nil } } after do |example| example.metadata[:response][:content] = { 'application/json' => { @@ -91,7 +91,7 @@ end response(422, 'Create an invitation with invalid reply_status parameters') do - let(:invitation) { { to_id: user1.id, from_id: user2.id, assignment_id: assignment.id, reply_status: "I" } } + let(:invitation) { { to_id: user1.id, from_id: user2.id, assignment_id: assignment.id, reply_status: 'I' } } after do |example| example.metadata[:response][:content] = { 'application/json' => { @@ -143,7 +143,7 @@ get('show invitation') do tags 'Invitations' response(404, 'show request with invalid invitation id') do - let(:id) { "INVALID" } + let(:id) { 'INVALID' } after do |example| example.metadata[:response][:content] = { @@ -155,6 +155,57 @@ run_test! end end - end + patch('update invitation') do + tags 'Invitation' + consumes 'application/json' + parameter name: :invitation_status, in: :body, schema: { + type: :object, + properties: { + reply_status: { type: :string } + }, + required: %w[] + } + + response(200, 'Accept invite') do + let(:id) { invitation.id } + let(:invitation_status) { { reply_status: Invitation::ACCEPT_STATUS } } + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + + response(200, 'Reject invite') do + let(:id) { invitation.id } + let(:invitation_status) { { reply_status: Invitation::REJECT_STATUS } } + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + + response(422, 'Invalid invite action') do + let(:id) { invitation.id } + let(:invitation_status) { { reply_status: 'Z' } } + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end end + From deb9cf7f819fee3ab1bc1e567eb4ef5319214632 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 18 Apr 2023 20:46:59 -0400 Subject: [PATCH 10/30] implement InvitationController#destroy using TDD --- .../api/v1/invitations_controller.rb | 6 ++- app/models/invitation.rb | 4 +- .../api/v1/invitation_controller_spec.rb | 45 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index df446397..d7288981 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -44,7 +44,11 @@ def update end # DELETE /api/v1/invitations/:id - def delete; end + def destroy + @invitation = Invitation.find(params[:id]) + Invitation.retract_invitation(@invitation, nil) + render json: { message: "Invitation with id #{params[:id]}, retracted" }, status: :ok + end # GET /invitations/:user_id/:assignment_id def list_all_invitations_for_user_assignment; end diff --git a/app/models/invitation.rb b/app/models/invitation.rb index cdb21a0c..b870419d 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -49,7 +49,9 @@ def self.decline_invitation(invitation, logged_in_user) end # This method handles all that need to be done upon an invitation retraction. - def self.retract_invitation(invite_id, logged_in_user); end + def self.retract_invitation(invitation, logged_in_user) + invitation.destroy + end # check if the user is invited def self.invited?(invitee_user_id, invited_user_id, assignment_id); end diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index 14eea9c6..dbcd93b6 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -205,6 +205,51 @@ end run_test! end + + response(404, 'Update status with invalid invitation_id') do + let(:id) { invitation.id + 10 } + let(:invitation_status) { { reply_status: 'A' } } + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + + delete('delete invitation with valid invite id') do + tags 'Invitation' + response(200, 'successful') do + let(:id) { invitation.id } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + delete('delete invitation with invalid invite id') do + tags 'Invitation' + response(404, 'successful') do + let(:id) { invitation.id + 100 } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end end end end From f58eaf5a69afd735ea5411a0ad894ac1b8a6b964 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 18 Apr 2023 21:55:43 -0400 Subject: [PATCH 11/30] implement InvitationController#list_all_invitations_for_user_assignment using TDD --- .../api/v1/invitations_controller.rb | 19 ++++- app/models/invitation.rb | 2 +- .../api/v1/invitation_controller_spec.rb | 76 ++++++++++++++++++- 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index d7288981..2a9ed109 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -51,7 +51,24 @@ def destroy end # GET /invitations/:user_id/:assignment_id - def list_all_invitations_for_user_assignment; end + def list_all_invitations_for_user_assignment + begin + @user = User.find(params[:user_id]) + rescue ActiveRecord::RecordNotFound => e + render json: { error: e.message }, status: :not_found + return + end + + begin + @assignment = Assignment.find(params[:assignment_id]) + rescue ActiveRecord::RecordNotFound => e + render json: { error: e.message }, status: :not_found + return + end + + @invitations = Invitation.where(to_id: @user.id).where(assignment_id: @assignment.id) + render json: @invitations, status: :ok + end private diff --git a/app/models/invitation.rb b/app/models/invitation.rb index b870419d..33a89d19 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -4,7 +4,7 @@ class Invitation < ApplicationRecord WAITING_STATUS = 'W' belongs_to :to_user, class_name: 'User', foreign_key: 'to_id', inverse_of: false belongs_to :from_user, class_name: 'User', foreign_key: 'from_id', inverse_of: false - belongs_to :assignment, class_name: 'Assignment', foreign_key: 'assignment_id' + belongs_to :assignment, class_name: 'Assignment', foreign_key: 'assignment_id' validates :reply_status, presence: true, length: { maximum: 1 } validates_inclusion_of :reply_status, in: [ACCEPT_STATUS, REJECT_STATUS, WAITING_STATUS], allow_nil: false validates :assignment_id, uniqueness: { diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index dbcd93b6..a8a187d0 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -252,5 +252,79 @@ end end end -end + path '/api/v1/invitations/{user_id}/{assignment_id}' do + parameter name: 'user_id', in: :path, type: :integer, description: 'id of user' + parameter name: 'assignment_id', in: :path, type: :integer, description: 'id of assignment' + + get('show all invitation with valid user and assignment') do + tags 'Invitations' + response(200, 'show all invitations for the user for an assignment') do + let(:user_id) { user1.id } + let(:assignment_id) { assignment.id } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + get('show invitation with invalid user and assignment') do + tags 'Invitations' + response(404, 'show all invitations for the user for an assignment') do + let(:user_id) { 'INVALID' } + let(:assignment_id) { assignment.id } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + + get('show invitation with user and invalid assignment') do + tags 'Invitations' + response(404, 'show all invitations for the user for an assignment') do + let(:user_id) { user1.id } + let(:assignment_id) { 'INVALID' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + get('show invitation with invalid user and invalid assignment') do + tags 'Invitations' + response(404, 'show all invitations for the user for an assignment') do + let(:user_id) { 'INVALID' } + let(:assignment_id) { 'INVALID' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + end +end From fd7ed8423af33e01cdad9f48c63c4536192ad52d Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 18 Apr 2023 22:10:36 -0400 Subject: [PATCH 12/30] refactor tests in invitation_spec.rb --- spec/models/invitation_spec.rb | 44 ++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/spec/models/invitation_spec.rb b/spec/models/invitation_spec.rb index 4a1802ea..532fcc28 100644 --- a/spec/models/invitation_spec.rb +++ b/spec/models/invitation_spec.rb @@ -1,41 +1,49 @@ require 'rails_helper' RSpec.describe Invitation, type: :model do - let(:user1) { build(:user, id: 4, name: 'no name', fullname: 'no two') } - let(:user2) { build(:user, id: 5, name: 'no name 2', fullname: 'no two 2') } - let(:assignment) { build(:assignment)} - - after(:each) do - ActionMailer::Base.deliveries.clear + let(:user1) { create :user, name: 'rohitgeddam' } + let(:user2) { create :user, name: 'superman' } + let(:invalid_user) { build :user, name: 'INVALID' } + let(:assignment) { create(:assignment) } + + it 'is default reply_status set to WAITING' do + invitation = Invitation.new(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + expect(invitation.reply_status).to eq('W') end - it "is valid with valid attributes" do - invitation = Invitation.new(to_user: user1, from_user: user2, assignment: assignment, reply_status: 'W') + it 'is valid with valid attributes' do + invitation = Invitation.new(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id, + reply_status: Invitation::WAITING_STATUS) expect(invitation).to be_valid end - it "is invalid with same from and to attribute" do - invitation = Invitation.new(to_user: user1, from_user: user1, assignment: assignment, reply_status: 'W') + it 'is invalid with same from and to attribute' do + invitation = Invitation.new(to_id: user1.id, from_id: user1.id, assignment_id: assignment.id, + reply_status: Invitation::WAITING_STATUS) expect(invitation).to_not be_valid end - it "is invalid with invalid to user attribute" do - invitation = Invitation.new(to_user: nil, from_user: user2, assignment: assignment, reply_status: 'W') + it 'is invalid with invalid to user attribute' do + invitation = Invitation.new(to_id: 'INVALID', from_id: user2.id, assignment_id: assignment.id, + reply_status: Invitation::WAITING_STATUS) expect(invitation).to_not be_valid end - it "is invalid with invalid from user attribute" do - invitation = Invitation.new(to_user: user1, from_user: nil, assignment: assignment, reply_status: 'W') + it 'is invalid with invalid from user attribute' do + invitation = Invitation.new(to_id: user1.id, from_id: 'INVALID', assignment_id: assignment.id, + reply_status: Invitation::WAITING_STATUS) expect(invitation).to_not be_valid end - it "is invalid with invalid assignment attribute" do - invitation = Invitation.new(to_user: user1, from_user: user2, assignment: nil, reply_status: 'W') + it 'is invalid with invalid assignment attribute' do + invitation = Invitation.new(to_id: user1.id, from_id: user2.id, assignment_id: 'INVALID', + reply_status: Invitation::WAITING_STATUS) expect(invitation).to_not be_valid end - it "is invalid with invalid reply_status attribute" do - invitation = Invitation.new(to_user: user1, from_user: user2, assignment: assignment, reply_status: 'X') + it 'is invalid with invalid reply_status attribute' do + invitation = Invitation.new(to_id: user1.id, from_id: user2.id, assignment_id: 'INVALID', + reply_status: 'X') expect(invitation).to_not be_valid end end From f1b54f69c96001d0dd6c34cb825918fcf2541f69 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 18 Apr 2023 22:10:54 -0400 Subject: [PATCH 13/30] implement set_defaults in invitation.rb --- app/models/invitation.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 33a89d19..ef6c94ec 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -1,7 +1,10 @@ class Invitation < ApplicationRecord + after_initialize :set_defaults + ACCEPT_STATUS = 'A' REJECT_STATUS = 'R' WAITING_STATUS = 'W' + belongs_to :to_user, class_name: 'User', foreign_key: 'to_id', inverse_of: false belongs_to :from_user, class_name: 'User', foreign_key: 'from_id', inverse_of: false belongs_to :assignment, class_name: 'Assignment', foreign_key: 'assignment_id' @@ -15,9 +18,9 @@ class Invitation < ApplicationRecord # validate if the to_id and from_id are same def to_from_cant_be_same - if self.from_id == self.to_id - errors.add(:from_id, 'to and from users should be different') - end + return unless from_id == to_id + + errors.add(:from_id, 'to and from users should be different') end # Return a new invitation @@ -69,6 +72,8 @@ def as_json(options = {}) end end - def set_defaults; end + def set_defaults + self.reply_status ||= WAITING_STATUS + end end From 13e296470543d94de58afaddb5f171d9c008382c Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 18 Apr 2023 22:27:03 -0400 Subject: [PATCH 14/30] implemented is_invited? in invitations.rb using TDD --- app/models/invitation.rb | 9 ++++++++- spec/models/invitation_spec.rb | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/models/invitation.rb b/app/models/invitation.rb index ef6c94ec..b0149000 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -57,7 +57,14 @@ def self.retract_invitation(invitation, logged_in_user) end # check if the user is invited - def self.invited?(invitee_user_id, invited_user_id, assignment_id); end + def self.invited?(from_id, to_id, assignment_id) + @invitations_count = Invitation.where(to_id: to_id) + .where(from_id: from_id) + .where(assignment_id: assignment_id) + .where(reply_status: WAITING_STATUS) + .count + @invitations_count > 0 + end # This will override the default as_json method in the ApplicationRecord class and specify def as_json(options = {}) diff --git a/spec/models/invitation_spec.rb b/spec/models/invitation_spec.rb index 532fcc28..698bf94b 100644 --- a/spec/models/invitation_spec.rb +++ b/spec/models/invitation_spec.rb @@ -6,6 +6,18 @@ let(:invalid_user) { build :user, name: 'INVALID' } let(:assignment) { create(:assignment) } + it 'is invited? false' do + invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + truth = Invitation.invited?(user1.id, user2.id, assignment.id) + expect(truth).to eq(false) + end + + it 'is invited? true' do + invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + truth = Invitation.invited?(user2.id, user1.id, assignment.id) + expect(truth).to eq(true) + end + it 'is default reply_status set to WAITING' do invitation = Invitation.new(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) expect(invitation.reply_status).to eq('W') From 7229f48b8ed40de639598fda445bbbd7e750749a Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Wed, 19 Apr 2023 11:26:14 -0400 Subject: [PATCH 15/30] implement send_invite_email and invitation_factory Setup InvitationSentMailer for the purpose of sending invitations. Implemented invitation_factory to help with creation of a new invitation with the given parameters. --- app/controllers/api/v1/invitations_controller.rb | 3 ++- app/mailers/invitation_sent_mailer.rb | 10 ++++++++++ app/models/invitation.rb | 10 ++++++++-- .../send_invitation_email.html.erb | 10 ++++++++++ spec/mailers/invitation_sent_spec.rb | 5 +++++ spec/mailers/previews/invitation_sent_preview.rb | 4 ++++ 6 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 app/mailers/invitation_sent_mailer.rb create mode 100644 app/views/invitation_sent_mailer/send_invitation_email.html.erb create mode 100644 spec/mailers/invitation_sent_spec.rb create mode 100644 spec/mailers/previews/invitation_sent_preview.rb diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index 2a9ed109..9ceeea50 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -10,8 +10,9 @@ def index # POST /api/v1/invitations/ def create params[:invitation][:reply_status] ||= Invitation::WAITING_STATUS - @invitation = Invitation.new(invite_params) + @invitation = Invitation.invitation_factory(invite_params) if @invitation.save + @invitation.send_invite_email render json: @invitation, status: :created else render json: @invitation.errors, status: :unprocessable_entity diff --git a/app/mailers/invitation_sent_mailer.rb b/app/mailers/invitation_sent_mailer.rb new file mode 100644 index 00000000..5dc8d2e3 --- /dev/null +++ b/app/mailers/invitation_sent_mailer.rb @@ -0,0 +1,10 @@ +class InvitationSentMailer < ApplicationMailer + default from: 'from@example.com' + def send_invitation_email + @invitation = params[:invitation] + p @invitation + @to_user = User.find(@invitation.to_id) + @from_user = User.find(@invitation.from_id) + mail(to: @to_user.email, subject: 'You have a new invitation from Expertiza') + end +end diff --git a/app/models/invitation.rb b/app/models/invitation.rb index b0149000..310c471e 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -25,10 +25,16 @@ def to_from_cant_be_same # Return a new invitation # params = :assignment_id, :to_id, :from_id, :reply_status - def invitation_factory(params); end + def self.invitation_factory(params) + Invitation.new(params) + end # send invite email - def send_invite_email; end + def send_invite_email + InvitationSentMailer.with(invitation: self) + .send_invitation_email + .deliver_later + end # Remove all invites sent by a user for an assignment. def self.remove_users_sent_invites_for_assignment(user_id, assignment_id); end diff --git a/app/views/invitation_sent_mailer/send_invitation_email.html.erb b/app/views/invitation_sent_mailer/send_invitation_email.html.erb new file mode 100644 index 00000000..bfe05d5f --- /dev/null +++ b/app/views/invitation_sent_mailer/send_invitation_email.html.erb @@ -0,0 +1,10 @@ + + + Invite from Expertiza + + + +

You have been invited to join the team by <%= @to_user.fullname %>

+ + + diff --git a/spec/mailers/invitation_sent_spec.rb b/spec/mailers/invitation_sent_spec.rb new file mode 100644 index 00000000..5d0363f9 --- /dev/null +++ b/spec/mailers/invitation_sent_spec.rb @@ -0,0 +1,5 @@ +require "rails_helper" + +RSpec.describe InvitationSentMailer, type: :mailer do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/mailers/previews/invitation_sent_preview.rb b/spec/mailers/previews/invitation_sent_preview.rb new file mode 100644 index 00000000..1bb8b6a9 --- /dev/null +++ b/spec/mailers/previews/invitation_sent_preview.rb @@ -0,0 +1,4 @@ +# Preview all emails at http://localhost:3000/rails/mailers/invitation_sent +class InvitationSentPreview < ActionMailer::Preview + +end From e63f2047d33dde13a59ce4c01c5650c4f71ea6dc Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Wed, 19 Apr 2023 12:25:19 -0400 Subject: [PATCH 16/30] refactor invitations_controller and invitations.rb converted class methods to instance methods. renamed list_all_invitations_for_user_assignment to invitations_for_user_assignment. --- .../api/v1/invitations_controller.rb | 8 ++-- app/models/invitation.rb | 42 +++++++++---------- config/routes.rb | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index 9ceeea50..8d9dc48e 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -33,10 +33,10 @@ def update @invitation = Invitation.find(@invite_id) case params[:reply_status] when Invitation::ACCEPT_STATUS - Invitation.accept_invitation(@invitation, nil) + @invitation.accept_invitation( nil) render json: @invitation, status: :ok when Invitation::REJECT_STATUS - Invitation.decline_invitation(@invitation, nil) + @invitation.decline_invitation( nil) render json: @invitation, status: :ok else render json: @invitation.errors, status: :unprocessable_entity @@ -47,12 +47,12 @@ def update # DELETE /api/v1/invitations/:id def destroy @invitation = Invitation.find(params[:id]) - Invitation.retract_invitation(@invitation, nil) + @invitation.retract_invitation(nil) render json: { message: "Invitation with id #{params[:id]}, retracted" }, status: :ok end # GET /invitations/:user_id/:assignment_id - def list_all_invitations_for_user_assignment + def invitations_for_user_assignment begin @user = User.find(params[:user_id]) rescue ActiveRecord::RecordNotFound => e diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 310c471e..079b3ae1 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -1,9 +1,9 @@ class Invitation < ApplicationRecord after_initialize :set_defaults - ACCEPT_STATUS = 'A' - REJECT_STATUS = 'R' - WAITING_STATUS = 'W' + ACCEPT_STATUS = 'A'.freeze + REJECT_STATUS = 'R'.freeze + WAITING_STATUS = 'W'.freeze belongs_to :to_user, class_name: 'User', foreign_key: 'to_id', inverse_of: false belongs_to :from_user, class_name: 'User', foreign_key: 'from_id', inverse_of: false @@ -29,6 +29,16 @@ def self.invitation_factory(params) Invitation.new(params) end + # check if the user is invited + def self.invited?(from_id, to_id, assignment_id) + @invitations_count = Invitation.where(to_id:) + .where(from_id:) + .where(assignment_id:) + .where(reply_status: WAITING_STATUS) + .count + @invitations_count.positive? + end + # send invite email def send_invite_email InvitationSentMailer.with(invitation: self) @@ -37,39 +47,29 @@ def send_invite_email end # Remove all invites sent by a user for an assignment. - def self.remove_users_sent_invites_for_assignment(user_id, assignment_id); end + def remove_users_sent_invites_for_assignment(user_id, assignment_id); end # After a users accepts an invite, the teams_users table needs to be updated. # NOTE: Depends on TeamUser model, which is not implemented yet. - def self.update_users_topic_after_invite_accept(inviter_user_id, invited_user_id, assignment_id); end + def update_users_topic_after_invite_accept(inviter_user_id, invited_user_id, assignment_id); end # This method handles all that needs to be done upon a user accepting an invitation. # First the users previous team is deleted if they were the only member of that # team and topics that the old team signed up for will be deleted. # Then invites the user that accepted the invite sent will be removed. # Last the users team entry will be added to the TeamsUser table and their assigned topic is updated - def self.accept_invitation(invitation, logged_in_user) - invitation.update(reply_status: ACCEPT_STATUS) + def accept_invitation(logged_in_user) + update(reply_status: ACCEPT_STATUS) end # This method handles all that needs to be done upon an user decline an invitation. - def self.decline_invitation(invitation, logged_in_user) - invitation.update(reply_status: REJECT_STATUS) + def decline_invitation(logged_in_user) + update(reply_status: REJECT_STATUS) end # This method handles all that need to be done upon an invitation retraction. - def self.retract_invitation(invitation, logged_in_user) - invitation.destroy - end - - # check if the user is invited - def self.invited?(from_id, to_id, assignment_id) - @invitations_count = Invitation.where(to_id: to_id) - .where(from_id: from_id) - .where(assignment_id: assignment_id) - .where(reply_status: WAITING_STATUS) - .count - @invitations_count > 0 + def retract_invitation(logged_in_user) + destroy end # This will override the default as_json method in the ApplicationRecord class and specify diff --git a/config/routes.rb b/config/routes.rb index 33e5ee74..6c48257d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,7 +15,7 @@ end resources :assignments resources :invitations do - get '/:user_id/:assignment_id/', on: :collection, action: :list_all_invitations_for_user_assignment + get '/:user_id/:assignment_id/', on: :collection, action: :invitations_for_user_assignment end end end From 3e282d43e96fb1caaa6fba74e595da50d63057bf Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Wed, 19 Apr 2023 12:28:27 -0400 Subject: [PATCH 17/30] Update invitation_controller_spec.rb --- spec/requests/api/v1/invitation_controller_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index a8a187d0..efac3266 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -157,7 +157,7 @@ end patch('update invitation') do - tags 'Invitation' + tags 'Invitations' consumes 'application/json' parameter name: :invitation_status, in: :body, schema: { type: :object, @@ -220,7 +220,7 @@ end delete('delete invitation with valid invite id') do - tags 'Invitation' + tags 'Invitations' response(200, 'successful') do let(:id) { invitation.id } @@ -236,7 +236,7 @@ end delete('delete invitation with invalid invite id') do - tags 'Invitation' + tags 'Invitations' response(404, 'successful') do let(:id) { invitation.id + 100 } From 6ef3cda3437bfeac3574ca3039323e1b821d25a7 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Wed, 19 Apr 2023 12:40:07 -0400 Subject: [PATCH 18/30] change InvitationController#destroy status code to 204 --- app/controllers/api/v1/invitations_controller.rb | 2 +- spec/requests/api/v1/invitation_controller_spec.rb | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index 8d9dc48e..6bc86b1e 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -48,7 +48,7 @@ def update def destroy @invitation = Invitation.find(params[:id]) @invitation.retract_invitation(nil) - render json: { message: "Invitation with id #{params[:id]}, retracted" }, status: :ok + render nothing: true, status: :no_content end # GET /invitations/:user_id/:assignment_id diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index efac3266..8850054d 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -221,16 +221,8 @@ delete('delete invitation with valid invite id') do tags 'Invitations' - response(200, 'successful') do + response(204, 'no content') do let(:id) { invitation.id } - - after do |example| - example.metadata[:response][:content] = { - 'application/json' => { - example: JSON.parse(response.body, symbolize_names: true) - } - } - end run_test! end end From 1b23ce786a466ea7e8970b4c9e38dac0407bc641 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Wed, 19 Apr 2023 12:47:02 -0400 Subject: [PATCH 19/30] Update invitation_controller_spec.rb update description in invitation_controller for swagger docs --- spec/requests/api/v1/invitation_controller_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index 8850054d..a93bd9f1 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -229,7 +229,7 @@ delete('delete invitation with invalid invite id') do tags 'Invitations' - response(404, 'successful') do + response(404, 'not found') do let(:id) { invitation.id + 100 } after do |example| @@ -268,7 +268,7 @@ get('show invitation with invalid user and assignment') do tags 'Invitations' - response(404, 'show all invitations for the user for an assignment') do + response(404, 'not found') do let(:user_id) { 'INVALID' } let(:assignment_id) { assignment.id } @@ -286,7 +286,7 @@ get('show invitation with user and invalid assignment') do tags 'Invitations' - response(404, 'show all invitations for the user for an assignment') do + response(404, 'not found') do let(:user_id) { user1.id } let(:assignment_id) { 'INVALID' } @@ -303,7 +303,7 @@ get('show invitation with invalid user and invalid assignment') do tags 'Invitations' - response(404, 'show all invitations for the user for an assignment') do + response(404, 'not found') do let(:user_id) { 'INVALID' } let(:assignment_id) { 'INVALID' } From b92413f8a5c12ac5e49d4a50f05e389f4a5300bc Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Thu, 20 Apr 2023 19:07:27 -0400 Subject: [PATCH 20/30] fix schemantics in invitations_controller_spec.rb --- .../api/v1/invitations_controller.rb | 2 +- .../api/v1/invitation_controller_spec.rb | 28 ++--- swagger/v1/swagger.yaml | 106 ++++++++++++++++++ 3 files changed, 121 insertions(+), 15 deletions(-) diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index 6bc86b1e..42129b09 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -15,7 +15,7 @@ def create @invitation.send_invite_email render json: @invitation, status: :created else - render json: @invitation.errors, status: :unprocessable_entity + render json: { error: @invitation.errors }, status: :unprocessable_entity end end diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index a93bd9f1..dd87ae99 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -14,7 +14,7 @@ tags 'Invitations' produces 'application/json' - response(200, 'successful') do + response(200, 'List all Invitations') do after do |example| example.metadata[:response][:content] = { @@ -90,7 +90,7 @@ run_test! end - response(422, 'Create an invitation with invalid reply_status parameters') do + response(422, 'Create an invitation with invalid reply_status parameter') do let(:invitation) { { to_id: user1.id, from_id: user2.id, assignment_id: assignment.id, reply_status: 'I' } } after do |example| example.metadata[:response][:content] = { @@ -125,7 +125,7 @@ get('show invitation') do tags 'Invitations' - response(200, 'show request with valid invitation id') do + response(200, 'Show invitation with valid invitation id') do let(:id) { invitation.id } after do |example| @@ -142,7 +142,7 @@ get('show invitation') do tags 'Invitations' - response(404, 'show request with invalid invitation id') do + response(404, 'Show invitation with invalid invitation id') do let(:id) { 'INVALID' } after do |example| @@ -167,7 +167,7 @@ required: %w[] } - response(200, 'Accept invite') do + response(200, 'Accept invite successfully') do let(:id) { invitation.id } let(:invitation_status) { { reply_status: Invitation::ACCEPT_STATUS } } after do |example| @@ -180,7 +180,7 @@ run_test! end - response(200, 'Reject invite') do + response(200, 'Reject invite successfully') do let(:id) { invitation.id } let(:invitation_status) { { reply_status: Invitation::REJECT_STATUS } } after do |example| @@ -193,7 +193,7 @@ run_test! end - response(422, 'Invalid invite action') do + response(422, 'Update invitation with invalid reply_status') do let(:id) { invitation.id } let(:invitation_status) { { reply_status: 'Z' } } after do |example| @@ -219,7 +219,7 @@ run_test! end - delete('delete invitation with valid invite id') do + delete('Delete invitation with valid invite id') do tags 'Invitations' response(204, 'no content') do let(:id) { invitation.id } @@ -227,7 +227,7 @@ end end - delete('delete invitation with invalid invite id') do + delete('Delete invitation with invalid invite id') do tags 'Invitations' response(404, 'not found') do let(:id) { invitation.id + 100 } @@ -249,9 +249,9 @@ parameter name: 'user_id', in: :path, type: :integer, description: 'id of user' parameter name: 'assignment_id', in: :path, type: :integer, description: 'id of assignment' - get('show all invitation with valid user and assignment') do + get('Show all invitation with valid user and assignment') do tags 'Invitations' - response(200, 'show all invitations for the user for an assignment') do + response(200, 'Show all invitations for the user for an assignment') do let(:user_id) { user1.id } let(:assignment_id) { assignment.id } @@ -266,7 +266,7 @@ end end - get('show invitation with invalid user and assignment') do + get('Show invitation with invalid user and assignment') do tags 'Invitations' response(404, 'not found') do let(:user_id) { 'INVALID' } @@ -284,7 +284,7 @@ end - get('show invitation with user and invalid assignment') do + get('Show invitation with user and invalid assignment') do tags 'Invitations' response(404, 'not found') do let(:user_id) { user1.id } @@ -301,7 +301,7 @@ end end - get('show invitation with invalid user and invalid assignment') do + get('Show invitation with invalid user and invalid assignment') do tags 'Invitations' response(404, 'not found') do let(:user_id) { 'INVALID' } diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 23401885..08ed8f08 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -4,6 +4,112 @@ info: title: EXPERTIZA API V1 version: v1 paths: + "/api/v1/invitations": + get: + summary: list invitations + tags: + - Invitations + responses: + '200': + description: List all Invitations + post: + summary: create invitation + tags: + - Invitations + parameters: [] + responses: + '201': + description: Create an invitation with valid parameters + '422': + description: Create an invitation with same to user and from user parameters + requestBody: + content: + application/json: + schema: + type: object + properties: + assignment_id: + type: integer + from_id: + type: integer + to_id: + type: integer + reply_status: + type: string + required: + - assignment_id + - from_id + - to_id + "/api/v1/invitations/{id}": + parameters: + - name: id + in: path + description: id of the invitation + required: true + schema: + type: integer + get: + summary: show invitation + tags: + - Invitations + responses: + '200': + description: Show invitation with valid invitation id + '404': + description: Show invitation with invalid invitation id + patch: + summary: update invitation + tags: + - Invitations + parameters: [] + responses: + '200': + description: Reject invite successfully + '422': + description: Update invitation with invalid reply_status + '404': + description: Update status with invalid invitation_id + requestBody: + content: + application/json: + schema: + type: object + properties: + reply_status: + type: string + required: [] + delete: + summary: Delete invitation with invalid invite id + tags: + - Invitations + responses: + '204': + description: no content + '404': + description: not found + "/api/v1/invitations/{user_id}/{assignment_id}": + parameters: + - name: user_id + in: path + description: id of user + required: true + schema: + type: integer + - name: assignment_id + in: path + description: id of assignment + required: true + schema: + type: integer + get: + summary: Show invitation with invalid user and invalid assignment + tags: + - Invitations + responses: + '200': + description: Show all invitations for the user for an assignment + '404': + description: not found "/api/v1/roles": get: summary: list roles From 15d3fc04f690208f4422f9ec6cad8ba5d52be137 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Sat, 22 Apr 2023 12:12:56 -0400 Subject: [PATCH 21/30] Update invitation.rb added _ before parameters to signify unused paramater --- app/models/invitation.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 079b3ae1..0dee477f 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -47,28 +47,29 @@ def send_invite_email end # Remove all invites sent by a user for an assignment. - def remove_users_sent_invites_for_assignment(user_id, assignment_id); end + def remove_users_sent_invites_for_assignment(_user_id, _assignment_id); end # After a users accepts an invite, the teams_users table needs to be updated. # NOTE: Depends on TeamUser model, which is not implemented yet. - def update_users_topic_after_invite_accept(inviter_user_id, invited_user_id, assignment_id); end + def update_users_topic_after_invite_accept(_inviter_user_id, _invited_user_id, _assignment_id); end # This method handles all that needs to be done upon a user accepting an invitation. - # First the users previous team is deleted if they were the only member of that + # Expected functionality: First the users previous team is deleted if they were the only member of that # team and topics that the old team signed up for will be deleted. # Then invites the user that accepted the invite sent will be removed. - # Last the users team entry will be added to the TeamsUser table and their assigned topic is updated - def accept_invitation(logged_in_user) + # Last the users team entry will be added to the TeamsUser table and their assigned topic is updated. + # For now it simply updates the invitation's reply_status. + def accept_invitation(_logged_in_user) update(reply_status: ACCEPT_STATUS) end # This method handles all that needs to be done upon an user decline an invitation. - def decline_invitation(logged_in_user) + def decline_invitation(_logged_in_user) update(reply_status: REJECT_STATUS) end # This method handles all that need to be done upon an invitation retraction. - def retract_invitation(logged_in_user) + def retract_invitation(_logged_in_user) destroy end @@ -88,5 +89,4 @@ def as_json(options = {}) def set_defaults self.reply_status ||= WAITING_STATUS end - end From 9fc0ec7ee8f5198df75f74f96f08dd03e61b9096 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Sat, 22 Apr 2023 12:13:52 -0400 Subject: [PATCH 22/30] add tests for Invitation model --- app/mailers/invitation_sent_mailer.rb | 1 - config/environments/test.rb | 3 ++- spec/models/invitation_spec.rb | 36 +++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/app/mailers/invitation_sent_mailer.rb b/app/mailers/invitation_sent_mailer.rb index 5dc8d2e3..ba522029 100644 --- a/app/mailers/invitation_sent_mailer.rb +++ b/app/mailers/invitation_sent_mailer.rb @@ -2,7 +2,6 @@ class InvitationSentMailer < ApplicationMailer default from: 'from@example.com' def send_invitation_email @invitation = params[:invitation] - p @invitation @to_user = User.find(@invitation.to_id) @from_user = User.find(@invitation.from_id) mail(to: @to_user.email, subject: 'You have a new invitation from Expertiza') diff --git a/config/environments/test.rb b/config/environments/test.rb index 5f6cef4d..37face99 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -42,7 +42,8 @@ # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test - + # This line let us to use expect(...).to have_enqueued_job.on_queue('mailers') + config.active_job.queue_adapter = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr diff --git a/spec/models/invitation_spec.rb b/spec/models/invitation_spec.rb index 698bf94b..1a64ae63 100644 --- a/spec/models/invitation_spec.rb +++ b/spec/models/invitation_spec.rb @@ -6,6 +6,42 @@ let(:invalid_user) { build :user, name: 'INVALID' } let(:assignment) { create(:assignment) } + + it 'is invitation_factory returning new Invitation' do + invitation = Invitation.invitation_factory(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + expect(invitation).to be_valid + end + + it 'sends a invitation email' do + invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + expect do + invitation.send_invite_email + end.to have_enqueued_job.on_queue('default').exactly(:once) + end + + it 'accepts invitation and change reply_status to Accept(A)' do + invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + invitation.accept_invitation(nil) + expect(invitation.reply_status).to eq(Invitation::ACCEPT_STATUS) + end + + it 'rejects invitation and change reply_status to Reject(R)' do + invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + invitation.decline_invitation(nil) + expect(invitation.reply_status).to eq(Invitation::REJECT_STATUS) + end + + it 'retracts invitation' do + invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + invitation.retract_invitation(nil) + expect { invitation.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'as_json works as expected' do + invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) + expect(invitation.as_json).to include('to_user', 'from_user', 'assignment', 'reply_status', 'id') + end + it 'is invited? false' do invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) truth = Invitation.invited?(user1.id, user2.id, assignment.id) From 778b43bc464f08932b69cd74955a660970fe4400 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Sat, 22 Apr 2023 12:40:13 -0400 Subject: [PATCH 23/30] Update invitaion_controller_spec.rb updated test case descriptions in invitation_controller_spec. --- .../api/v1/invitation_controller_spec.rb | 71 ++++++------------- swagger/v1/swagger.yaml | 26 +++---- 2 files changed, 33 insertions(+), 64 deletions(-) diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index dd87ae99..2f7eea0f 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -13,9 +13,7 @@ get('list invitations') do tags 'Invitations' produces 'application/json' - - response(200, 'List all Invitations') do - + response(200, 'Success') do after do |example| example.metadata[:response][:content] = { 'application/json' => { @@ -23,7 +21,6 @@ } } end - run_test! end end @@ -42,7 +39,7 @@ required: %w[assignment_id from_id to_id] } - response(201, 'Create an invitation with valid parameters') do + response(201, 'Create successful') do let(:invitation) { { to_id: user1.id, from_id: user2.id, assignment_id: assignment.id } } after do |example| example.metadata[:response][:content] = { @@ -54,7 +51,7 @@ run_test! end - response(422, 'Create an invitation with invalid to user parameters') do + response(422, 'Invalid request') do let(:invitation) { { to_id: invalid_user.id, from_id: user2.id, assignment_id: assignment.id } } after do |example| example.metadata[:response][:content] = { @@ -66,7 +63,7 @@ run_test! end - response(422, 'Create an invitation with invalid from user parameters') do + response(422, 'Invalid request') do let(:invitation) { { to_id: user1.id, from_id: invalid_user.id, assignment_id: assignment.id } } after do |example| example.metadata[:response][:content] = { @@ -78,7 +75,7 @@ run_test! end - response(422, 'Create an invitation with invalid assignment parameters') do + response(422, 'Invalid request') do let(:invitation) { { to_id: user1.id, from_id: user2.id, assignment_id: nil } } after do |example| example.metadata[:response][:content] = { @@ -90,7 +87,7 @@ run_test! end - response(422, 'Create an invitation with invalid reply_status parameter') do + response(422, 'Invalid request') do let(:invitation) { { to_id: user1.id, from_id: user2.id, assignment_id: assignment.id, reply_status: 'I' } } after do |example| example.metadata[:response][:content] = { @@ -102,7 +99,7 @@ run_test! end - response(422, 'Create an invitation with same to user and from user parameters') do + response(422, 'Invalid request') do let(:invitation) { { to_id: user1.id, from_id: user1.id, assignment_id: assignment.id } } after do |example| example.metadata[:response][:content] = { @@ -113,21 +110,15 @@ end run_test! end - - end - end - path '/api/v1/invitations/{id}' do parameter name: 'id', in: :path, type: :integer, description: 'id of the invitation' - get('show invitation') do tags 'Invitations' - response(200, 'Show invitation with valid invitation id') do + response(200, 'Show invitation') do let(:id) { invitation.id } - after do |example| example.metadata[:response][:content] = { 'application/json' => { @@ -138,11 +129,7 @@ run_test! end - end - - get('show invitation') do - tags 'Invitations' - response(404, 'Show invitation with invalid invitation id') do + response(404, 'Not found') do let(:id) { 'INVALID' } after do |example| @@ -167,7 +154,7 @@ required: %w[] } - response(200, 'Accept invite successfully') do + response(200, 'Update successful') do let(:id) { invitation.id } let(:invitation_status) { { reply_status: Invitation::ACCEPT_STATUS } } after do |example| @@ -180,7 +167,7 @@ run_test! end - response(200, 'Reject invite successfully') do + response(200, 'Update successful') do let(:id) { invitation.id } let(:invitation_status) { { reply_status: Invitation::REJECT_STATUS } } after do |example| @@ -193,7 +180,7 @@ run_test! end - response(422, 'Update invitation with invalid reply_status') do + response(422, 'Invalid request') do let(:id) { invitation.id } let(:invitation_status) { { reply_status: 'Z' } } after do |example| @@ -206,7 +193,7 @@ run_test! end - response(404, 'Update status with invalid invitation_id') do + response(404, 'Not found') do let(:id) { invitation.id + 10 } let(:invitation_status) { { reply_status: 'A' } } after do |example| @@ -219,17 +206,14 @@ run_test! end - delete('Delete invitation with valid invite id') do + delete('Delete invitation') do tags 'Invitations' - response(204, 'no content') do + response(204, 'Delete successful') do let(:id) { invitation.id } run_test! end - end - delete('Delete invitation with invalid invite id') do - tags 'Invitations' - response(404, 'not found') do + response(404, 'Not found') do let(:id) { invitation.id + 100 } after do |example| @@ -248,8 +232,7 @@ path '/api/v1/invitations/{user_id}/{assignment_id}' do parameter name: 'user_id', in: :path, type: :integer, description: 'id of user' parameter name: 'assignment_id', in: :path, type: :integer, description: 'id of assignment' - - get('Show all invitation with valid user and assignment') do + get('Show all invitation for the given user and assignment') do tags 'Invitations' response(200, 'Show all invitations for the user for an assignment') do let(:user_id) { user1.id } @@ -264,14 +247,10 @@ end run_test! end - end - get('Show invitation with invalid user and assignment') do - tags 'Invitations' - response(404, 'not found') do + response(404, 'Not found') do let(:user_id) { 'INVALID' } let(:assignment_id) { assignment.id } - after do |example| example.metadata[:response][:content] = { 'application/json' => { @@ -281,15 +260,10 @@ end run_test! end - end - - get('Show invitation with user and invalid assignment') do - tags 'Invitations' - response(404, 'not found') do + response(404, 'Not found') do let(:user_id) { user1.id } let(:assignment_id) { 'INVALID' } - after do |example| example.metadata[:response][:content] = { 'application/json' => { @@ -299,14 +273,10 @@ end run_test! end - end - get('Show invitation with invalid user and invalid assignment') do - tags 'Invitations' - response(404, 'not found') do + response(404, 'Not found') do let(:user_id) { 'INVALID' } let(:assignment_id) { 'INVALID' } - after do |example| example.metadata[:response][:content] = { 'application/json' => { @@ -317,6 +287,5 @@ run_test! end end - end end diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 08ed8f08..6785803e 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -11,7 +11,7 @@ paths: - Invitations responses: '200': - description: List all Invitations + description: Success post: summary: create invitation tags: @@ -19,9 +19,9 @@ paths: parameters: [] responses: '201': - description: Create an invitation with valid parameters + description: Create successful '422': - description: Create an invitation with same to user and from user parameters + description: Invalid request requestBody: content: application/json: @@ -54,9 +54,9 @@ paths: - Invitations responses: '200': - description: Show invitation with valid invitation id + description: Show invitation '404': - description: Show invitation with invalid invitation id + description: Not found patch: summary: update invitation tags: @@ -64,11 +64,11 @@ paths: parameters: [] responses: '200': - description: Reject invite successfully + description: Update successful '422': - description: Update invitation with invalid reply_status + description: Invalid request '404': - description: Update status with invalid invitation_id + description: Not found requestBody: content: application/json: @@ -79,14 +79,14 @@ paths: type: string required: [] delete: - summary: Delete invitation with invalid invite id + summary: Delete invitation tags: - Invitations responses: '204': - description: no content + description: Delete successful '404': - description: not found + description: Not found "/api/v1/invitations/{user_id}/{assignment_id}": parameters: - name: user_id @@ -102,14 +102,14 @@ paths: schema: type: integer get: - summary: Show invitation with invalid user and invalid assignment + summary: Show all invitation for the given user and assignment tags: - Invitations responses: '200': description: Show all invitations for the user for an assignment '404': - description: not found + description: Not found "/api/v1/roles": get: summary: list roles From 2610af49fee23aed83bbe1322392e21a28d5aeec Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Sun, 23 Apr 2023 09:53:12 -0400 Subject: [PATCH 24/30] Update factories.rb Fix Rubocop issues --- spec/factories.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/factories.rb b/spec/factories.rb index cac30aad..056d95ce 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -1,9 +1,9 @@ FactoryBot.define do factory :user do - sequence(:name) { |n| "#{Faker::Name.name}".delete(" \t\r\n").downcase } - sequence(:email) { |n| "#{Faker::Internet.email}"} - password { "blahblahblah" } + sequence(:name) { |n| Faker::Name.name.to_s.delete(" \t\r\n").downcase } + sequence(:email) { |n| Faker::Internet.email.to_s} + password { 'password' } sequence(:fullname) { |n| "#{Faker::Name.name}#{Faker::Name.name}".downcase } role factory: :role end @@ -13,13 +13,13 @@ end factory :assignment do - name { (Assignment.last ? ('assignment' + (Assignment.last.id + 1).to_s) : 'final2').to_s } + name { (Assignment.last ? "assignment#{(Assignment.last.id + 1).to_s}" : 'final2').to_s } end factory :invitation do from_user factory: :user to_user factory: :user assignment factory: :assignment - reply_status { "W" } + reply_status { 'W' } end -end \ No newline at end of file +end From b0201b3940ff7df5ca706dbca3699d756fc7a8d9 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Sun, 23 Apr 2023 10:08:34 -0400 Subject: [PATCH 25/30] Updated comments updated comments in invitation_controller.rb, invitation.rb for better understanding of the purpose of the unimplemented methods. --- app/controllers/api/v1/invitations_controller.rb | 10 +++++++--- app/models/invitation.rb | 6 +++--- spec/models/invitation_spec.rb | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index 42129b09..0ba3a547 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -73,18 +73,22 @@ def invitations_for_user_assignment private - # This method will check if the invited user exists. + # This method will check if the invited user exists.Additionally, this + # method will also check if the sender himself is participating in the given assignment + # before they can send an invitation. def check_invited_user_before_invitation; end # This method will check if the invited user is a participant in the assignment. + # Currently there is no association between assignment and users therefore this method is not implemented yet. def check_participant_before_invitation; end # This method will check if the team meets the joining requirement before sending an invite. # NOTE: This method depends on TeamUser and AssignmentTeam, which is not implemented yet. def check_team_before_invitation; end - # This method will check if the team meets the joining requirements - # when an invitation is being accepted + # This method will check if the team meets the joining requirements when an invitation + # is being accepted for example check if the invite's team is still existing, + # and have available slot to add the invitee. # NOTE: This method depends on AssignmentParticipant and AssignmentTeam # which is not implemented yet. def check_team_before_accept; end diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 0dee477f..db67ece7 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -57,13 +57,13 @@ def update_users_topic_after_invite_accept(_inviter_user_id, _invited_user_id, _ # Expected functionality: First the users previous team is deleted if they were the only member of that # team and topics that the old team signed up for will be deleted. # Then invites the user that accepted the invite sent will be removed. - # Last the users team entry will be added to the TeamsUser table and their assigned topic is updated. - # For now it simply updates the invitation's reply_status. + # Lastly the users team entry will be added to the TeamsUser table and their assigned topic is updated. + # NOTE: For now this method simply updates the invitation's reply_status. def accept_invitation(_logged_in_user) update(reply_status: ACCEPT_STATUS) end - # This method handles all that needs to be done upon an user decline an invitation. + # This method handles all that needs to be done upon an user declining an invitation. def decline_invitation(_logged_in_user) update(reply_status: REJECT_STATUS) end diff --git a/spec/models/invitation_spec.rb b/spec/models/invitation_spec.rb index 1a64ae63..ecc345c2 100644 --- a/spec/models/invitation_spec.rb +++ b/spec/models/invitation_spec.rb @@ -12,7 +12,7 @@ expect(invitation).to be_valid end - it 'sends a invitation email' do + it 'sends an invitation email' do invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) expect do invitation.send_invite_email From 87c993249fad1fb6409e7828cff78c74dcab6f29 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Sun, 23 Apr 2023 10:56:57 -0400 Subject: [PATCH 26/30] Update invitation.rb removed unused method remove_users_sent_invites_for_assignment --- app/models/invitation.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/models/invitation.rb b/app/models/invitation.rb index db67ece7..399d0164 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -46,9 +46,6 @@ def send_invite_email .deliver_later end - # Remove all invites sent by a user for an assignment. - def remove_users_sent_invites_for_assignment(_user_id, _assignment_id); end - # After a users accepts an invite, the teams_users table needs to be updated. # NOTE: Depends on TeamUser model, which is not implemented yet. def update_users_topic_after_invite_accept(_inviter_user_id, _invited_user_id, _assignment_id); end From 1d2f25190692e934d02294440e316424765f8c3f Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 2 May 2023 19:01:04 -0400 Subject: [PATCH 27/30] Change Invitation api route --- app/controllers/api/v1/invitations_controller.rb | 2 +- config/routes.rb | 2 +- spec/requests/api/v1/invitation_controller_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index 0ba3a547..a82d0cd8 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -75,7 +75,7 @@ def invitations_for_user_assignment # This method will check if the invited user exists.Additionally, this # method will also check if the sender himself is participating in the given assignment - # before they can send an invitation. + # before they can send an invitation. def check_invited_user_before_invitation; end # This method will check if the invited user is a participant in the assignment. diff --git a/config/routes.rb b/config/routes.rb index 6c48257d..e8aaadab 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,7 +15,7 @@ end resources :assignments resources :invitations do - get '/:user_id/:assignment_id/', on: :collection, action: :invitations_for_user_assignment + get 'user/:user_id/assignment/:assignment_id/', on: :collection, action: :invitations_for_user_assignment end end end diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index 2f7eea0f..b676c6cb 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -229,7 +229,7 @@ end end - path '/api/v1/invitations/{user_id}/{assignment_id}' do + path '/api/v1/invitations/user/{user_id}/assignment/{assignment_id}' do parameter name: 'user_id', in: :path, type: :integer, description: 'id of user' parameter name: 'assignment_id', in: :path, type: :integer, description: 'id of assignment' get('Show all invitation for the given user and assignment') do From 449bc86ac8b55b67e1356faaf1ed5f50494abe95 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Tue, 2 May 2023 23:39:55 -0400 Subject: [PATCH 28/30] remove deprecated methods from InvitationsController --- app/controllers/api/v1/invitations_controller.rb | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index a82d0cd8..71980f6c 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -73,26 +73,10 @@ def invitations_for_user_assignment private - # This method will check if the invited user exists.Additionally, this - # method will also check if the sender himself is participating in the given assignment - # before they can send an invitation. - def check_invited_user_before_invitation; end - # This method will check if the invited user is a participant in the assignment. # Currently there is no association between assignment and users therefore this method is not implemented yet. def check_participant_before_invitation; end - # This method will check if the team meets the joining requirement before sending an invite. - # NOTE: This method depends on TeamUser and AssignmentTeam, which is not implemented yet. - def check_team_before_invitation; end - - # This method will check if the team meets the joining requirements when an invitation - # is being accepted for example check if the invite's team is still existing, - # and have available slot to add the invitee. - # NOTE: This method depends on AssignmentParticipant and AssignmentTeam - # which is not implemented yet. - def check_team_before_accept; end - # only allow a list of valid invite params def invite_params params.require(:invitation).permit(:id, :assignment_id, :from_id, :to_id, :reply_status) From f7613d7c9fff5d82760da1a5cf164211b841563e Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Wed, 3 May 2023 19:24:00 -0400 Subject: [PATCH 29/30] move invitation model validations to invitation_validator moved invitation model validations to invitation_validator. updated invitation spec files. --- app/models/invitation.rb | 37 ++++---------- app/validators/invitation_validator.rb | 50 +++++++++++++++++++ spec/models/invitation_spec.rb | 14 +++--- .../api/v1/invitation_controller_spec.rb | 4 +- 4 files changed, 70 insertions(+), 35 deletions(-) create mode 100644 app/validators/invitation_validator.rb diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 399d0164..aff3edb0 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -1,27 +1,11 @@ class Invitation < ApplicationRecord after_initialize :set_defaults - ACCEPT_STATUS = 'A'.freeze - REJECT_STATUS = 'R'.freeze - WAITING_STATUS = 'W'.freeze - belongs_to :to_user, class_name: 'User', foreign_key: 'to_id', inverse_of: false belongs_to :from_user, class_name: 'User', foreign_key: 'from_id', inverse_of: false belongs_to :assignment, class_name: 'Assignment', foreign_key: 'assignment_id' - validates :reply_status, presence: true, length: { maximum: 1 } - validates_inclusion_of :reply_status, in: [ACCEPT_STATUS, REJECT_STATUS, WAITING_STATUS], allow_nil: false - validates :assignment_id, uniqueness: { - scope: %i[from_id to_id reply_status], - message: 'You cannot have duplicate invitations' - } - validate :to_from_cant_be_same - - # validate if the to_id and from_id are same - def to_from_cant_be_same - return unless from_id == to_id - errors.add(:from_id, 'to and from users should be different') - end + validates_with InvitationValidator # Return a new invitation # params = :assignment_id, :to_id, :from_id, :reply_status @@ -31,12 +15,13 @@ def self.invitation_factory(params) # check if the user is invited def self.invited?(from_id, to_id, assignment_id) - @invitations_count = Invitation.where(to_id:) - .where(from_id:) - .where(assignment_id:) - .where(reply_status: WAITING_STATUS) - .count - @invitations_count.positive? + conditions = { + to_id:, + from_id:, + assignment_id:, + reply_status: InvitationValidator::WAITING_STATUS + } + @invitations_exist = Invitation.where(conditions).exists? end # send invite email @@ -57,12 +42,12 @@ def update_users_topic_after_invite_accept(_inviter_user_id, _invited_user_id, _ # Lastly the users team entry will be added to the TeamsUser table and their assigned topic is updated. # NOTE: For now this method simply updates the invitation's reply_status. def accept_invitation(_logged_in_user) - update(reply_status: ACCEPT_STATUS) + update(reply_status: InvitationValidator::ACCEPT_STATUS) end # This method handles all that needs to be done upon an user declining an invitation. def decline_invitation(_logged_in_user) - update(reply_status: REJECT_STATUS) + update(reply_status: InvitationValidator::REJECT_STATUS) end # This method handles all that need to be done upon an invitation retraction. @@ -84,6 +69,6 @@ def as_json(options = {}) end def set_defaults - self.reply_status ||= WAITING_STATUS + self.reply_status ||= InvitationValidator::WAITING_STATUS end end diff --git a/app/validators/invitation_validator.rb b/app/validators/invitation_validator.rb new file mode 100644 index 00000000..be54a086 --- /dev/null +++ b/app/validators/invitation_validator.rb @@ -0,0 +1,50 @@ +# app/validators/invitation_validator.rb +class InvitationValidator < ActiveModel::Validator + ACCEPT_STATUS = 'A'.freeze + REJECT_STATUS = 'R'.freeze + WAITING_STATUS = 'W'.freeze + + DUPLICATE_INVITATION_ERROR_MSG = 'You cannot have duplicate invitations'.freeze + TO_FROM_SAME_ERROR_MSG = 'to and from users should be different'.freeze + REPLY_STATUS_ERROR_MSG = 'must be present and have a maximum length of 1'.freeze + REPLY_STATUS_INCLUSION_ERROR_MSG = "must be one of #{[ACCEPT_STATUS, REJECT_STATUS, WAITING_STATUS].to_sentence}".freeze + + def validate(record) + validate_reply_status(record) + validate_reply_status_inclusion(record) + validate_duplicate_invitation(record) + validate_to_from_different(record) + end + + private + + def validate_reply_status(record) + unless record.reply_status.present? && record.reply_status.length <= 1 + record.errors.add(:reply_status, REPLY_STATUS_ERROR_MSG) + end + end + + def validate_reply_status_inclusion(record) + unless [ACCEPT_STATUS, REJECT_STATUS, WAITING_STATUS].include?(record.reply_status) + record.errors.add(:reply_status, REPLY_STATUS_INCLUSION_ERROR_MSG) + end + end + + def validate_duplicate_invitation(record) + conditions = { + to_id: record.to_id, + from_id: record.from_id, + assignment_id: record.assignment_id, + reply_status: record.reply_status + } + if Invitation.where(conditions).exists? + record.errors[:base] << DUPLICATE_INVITATION_ERROR_MSG + end + end + + def validate_to_from_different(record) + if record.from_id == record.to_id + record.errors.add(:from_id, TO_FROM_SAME_ERROR_MSG) + end + end +end \ No newline at end of file diff --git a/spec/models/invitation_spec.rb b/spec/models/invitation_spec.rb index ecc345c2..1d1f17ab 100644 --- a/spec/models/invitation_spec.rb +++ b/spec/models/invitation_spec.rb @@ -22,13 +22,13 @@ it 'accepts invitation and change reply_status to Accept(A)' do invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) invitation.accept_invitation(nil) - expect(invitation.reply_status).to eq(Invitation::ACCEPT_STATUS) + expect(invitation.reply_status).to eq(InvitationValidator::ACCEPT_STATUS) end it 'rejects invitation and change reply_status to Reject(R)' do invitation = Invitation.create(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id) invitation.decline_invitation(nil) - expect(invitation.reply_status).to eq(Invitation::REJECT_STATUS) + expect(invitation.reply_status).to eq(InvitationValidator::REJECT_STATUS) end it 'retracts invitation' do @@ -61,31 +61,31 @@ it 'is valid with valid attributes' do invitation = Invitation.new(to_id: user1.id, from_id: user2.id, assignment_id: assignment.id, - reply_status: Invitation::WAITING_STATUS) + reply_status: InvitationValidator::WAITING_STATUS) expect(invitation).to be_valid end it 'is invalid with same from and to attribute' do invitation = Invitation.new(to_id: user1.id, from_id: user1.id, assignment_id: assignment.id, - reply_status: Invitation::WAITING_STATUS) + reply_status: InvitationValidator::WAITING_STATUS) expect(invitation).to_not be_valid end it 'is invalid with invalid to user attribute' do invitation = Invitation.new(to_id: 'INVALID', from_id: user2.id, assignment_id: assignment.id, - reply_status: Invitation::WAITING_STATUS) + reply_status: InvitationValidator::WAITING_STATUS) expect(invitation).to_not be_valid end it 'is invalid with invalid from user attribute' do invitation = Invitation.new(to_id: user1.id, from_id: 'INVALID', assignment_id: assignment.id, - reply_status: Invitation::WAITING_STATUS) + reply_status: InvitationValidator::WAITING_STATUS) expect(invitation).to_not be_valid end it 'is invalid with invalid assignment attribute' do invitation = Invitation.new(to_id: user1.id, from_id: user2.id, assignment_id: 'INVALID', - reply_status: Invitation::WAITING_STATUS) + reply_status: InvitationValidator::WAITING_STATUS) expect(invitation).to_not be_valid end diff --git a/spec/requests/api/v1/invitation_controller_spec.rb b/spec/requests/api/v1/invitation_controller_spec.rb index b676c6cb..744dc80f 100644 --- a/spec/requests/api/v1/invitation_controller_spec.rb +++ b/spec/requests/api/v1/invitation_controller_spec.rb @@ -156,7 +156,7 @@ response(200, 'Update successful') do let(:id) { invitation.id } - let(:invitation_status) { { reply_status: Invitation::ACCEPT_STATUS } } + let(:invitation_status) { { reply_status: InvitationValidator::ACCEPT_STATUS } } after do |example| example.metadata[:response][:content] = { 'application/json' => { @@ -169,7 +169,7 @@ response(200, 'Update successful') do let(:id) { invitation.id } - let(:invitation_status) { { reply_status: Invitation::REJECT_STATUS } } + let(:invitation_status) { { reply_status: InvitationValidator::REJECT_STATUS } } after do |example| example.metadata[:response][:content] = { 'application/json' => { From 3a3a8a64b99ae3c5ce709dcc92e63d7076db14d9 Mon Sep 17 00:00:00 2001 From: rohitgeddam Date: Wed, 3 May 2023 19:25:53 -0400 Subject: [PATCH 30/30] make code style consistent in InvitationController --- .../api/v1/invitations_controller.rb | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/controllers/api/v1/invitations_controller.rb b/app/controllers/api/v1/invitations_controller.rb index 71980f6c..1b4d88f1 100644 --- a/app/controllers/api/v1/invitations_controller.rb +++ b/app/controllers/api/v1/invitations_controller.rb @@ -4,12 +4,12 @@ class Api::V1::InvitationsController < ApplicationController # GET /api/v1/invitations def index @invitations = Invitation.all - render json: @invitations + render json: @invitations, status: :ok end # POST /api/v1/invitations/ def create - params[:invitation][:reply_status] ||= Invitation::WAITING_STATUS + params[:invitation][:reply_status] ||= InvitationValidator::WAITING_STATUS @invitation = Invitation.invitation_factory(invite_params) if @invitation.save @invitation.send_invite_email @@ -23,20 +23,17 @@ def create def show @invitation = Invitation.find(params[:id]) render json: @invitation, status: :ok - rescue ActiveRecord::RecordNotFound => e - render json: { error: e.message }, status: :not_found end # PATCH /api/v1/invitations/:id def update - @invite_id = params[:id] - @invitation = Invitation.find(@invite_id) + @invitation = Invitation.find(params[:id]) case params[:reply_status] - when Invitation::ACCEPT_STATUS - @invitation.accept_invitation( nil) + when InvitationValidator::ACCEPT_STATUS + @invitation.accept_invitation(nil) render json: @invitation, status: :ok - when Invitation::REJECT_STATUS - @invitation.decline_invitation( nil) + when InvitationValidator::REJECT_STATUS + @invitation.decline_invitation(nil) render json: @invitation, status: :ok else render json: @invitation.errors, status: :unprocessable_entity