Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

E2330. Reimplement Invitation Controller #32

Merged
merged 31 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
90b01b0
add invitation model and invitation model tests
rohitgeddam Apr 12, 2023
f01418a
Update Gemfile.lock
rohitgeddam Apr 13, 2023
e70a4d3
add method signatures
rohitgeddam Apr 13, 2023
c7256fa
implement index route with TDD
rohitgeddam Apr 18, 2023
942665b
implement InvitationController#create using TDD
rohitgeddam Apr 18, 2023
3ed5450
add more tests for InvitationController
rohitgeddam Apr 18, 2023
42c4b93
add Invitation factory
rohitgeddam Apr 18, 2023
8695e57
implement InvitationController#show using TDD
rohitgeddam Apr 18, 2023
e48982c
implement InvitationController#update using TDD
rohitgeddam Apr 19, 2023
deb9cf7
implement InvitationController#destroy using TDD
rohitgeddam Apr 19, 2023
f58eaf5
implement InvitationController#list_all_invitations_for_user_assignme…
rohitgeddam Apr 19, 2023
fd7ed84
refactor tests in invitation_spec.rb
rohitgeddam Apr 19, 2023
f1b54f6
implement set_defaults in invitation.rb
rohitgeddam Apr 19, 2023
13e2964
implemented is_invited? in invitations.rb using TDD
rohitgeddam Apr 19, 2023
7229f48
implement send_invite_email and invitation_factory
rohitgeddam Apr 19, 2023
e63f204
refactor invitations_controller and invitations.rb
rohitgeddam Apr 19, 2023
3e282d4
Update invitation_controller_spec.rb
rohitgeddam Apr 19, 2023
6ef3cda
change InvitationController#destroy status code to 204
rohitgeddam Apr 19, 2023
1b23ce7
Update invitation_controller_spec.rb
rohitgeddam Apr 19, 2023
b92413f
fix schemantics in invitations_controller_spec.rb
rohitgeddam Apr 20, 2023
15d3fc0
Update invitation.rb
rohitgeddam Apr 22, 2023
9fc0ec7
add tests for Invitation model
rohitgeddam Apr 22, 2023
778b43b
Update invitaion_controller_spec.rb
rohitgeddam Apr 22, 2023
2610af4
Update factories.rb
rohitgeddam Apr 23, 2023
b0201b3
Updated comments
rohitgeddam Apr 23, 2023
87c9932
Update invitation.rb
rohitgeddam Apr 23, 2023
1d2f251
Change Invitation api route
rohitgeddam May 2, 2023
449bc86
remove deprecated methods from InvitationsController
rohitgeddam May 3, 2023
f7613d7
move invitation model validations to invitation_validator
rohitgeddam May 3, 2023
3a3a8a6
make code style consistent in InvitationController
rohitgeddam May 3, 2023
e1c7e94
Merge branch 'main' into main
mundra-ankur Jun 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ group :development, :test do
gem 'simplecov', require: false, group: :test
gem 'rspec-rails'
gem 'rswag-specs'
gem 'factory_bot_rails'
gem 'faker'
end

group :development do
Expand Down
12 changes: 12 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ 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)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
globalid (1.1.0)
activesupport (>= 5.0)
i18n (1.12.0)
Expand Down Expand Up @@ -111,6 +118,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)
Expand Down Expand Up @@ -213,12 +222,15 @@ GEM
zeitwerk (2.6.7)

PLATFORMS
x86_64-darwin-22
x86_64-linux

DEPENDENCIES
bcrypt (~> 3.1.7)
bootsnap
debug
factory_bot_rails
faker
mysql2 (~> 0.5.5)
puma (~> 5.0)
rack-cors
Expand Down
87 changes: 87 additions & 0 deletions app/controllers/api/v1/invitations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
class Api::V1::InvitationsController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, with: :invite_not_found

# GET /api/v1/invitations
def index
@invitations = Invitation.all
render json: @invitations, status: :ok
end

# POST /api/v1/invitations/
def create
params[:invitation][:reply_status] ||= InvitationValidator::WAITING_STATUS
@invitation = Invitation.invitation_factory(invite_params)
if @invitation.save
@invitation.send_invite_email
render json: @invitation, status: :created
else
render json: { error: @invitation.errors }, status: :unprocessable_entity
end
end

# GET /api/v1/invitations/:id
def show
@invitation = Invitation.find(params[:id])
render json: @invitation, status: :ok
end

# PATCH /api/v1/invitations/:id
def update
@invitation = Invitation.find(params[:id])
case params[:reply_status]
when InvitationValidator::ACCEPT_STATUS
@invitation.accept_invitation(nil)
render json: @invitation, status: :ok
when InvitationValidator::REJECT_STATUS
@invitation.decline_invitation(nil)
render json: @invitation, status: :ok
else
render json: @invitation.errors, status: :unprocessable_entity
end

end

# DELETE /api/v1/invitations/:id
def destroy
@invitation = Invitation.find(params[:id])
@invitation.retract_invitation(nil)
render nothing: true, status: :no_content
end

# GET /invitations/:user_id/:assignment_id
def 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

# This method will check if the invited user is a participant in the assignment.
rohitgeddam marked this conversation as resolved.
Show resolved Hide resolved
# Currently there is no association between assignment and users therefore this method is not implemented yet.
def check_participant_before_invitation; 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)
end

# helper method used when invite is not found
def invite_not_found
render json: { error: "Invitation with id #{params[:id]} not found" }, status: :not_found
end

end
9 changes: 9 additions & 0 deletions app/mailers/invitation_sent_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class InvitationSentMailer < ApplicationMailer
default from: 'from@example.com'
def send_invitation_email
@invitation = params[: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
1 change: 1 addition & 0 deletions app/models/assignment.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
class Assignment < ApplicationRecord
has_many :invitations
end
74 changes: 74 additions & 0 deletions app/models/invitation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
class Invitation < ApplicationRecord
after_initialize :set_defaults

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'
mundra-ankur marked this conversation as resolved.
Show resolved Hide resolved

validates_with InvitationValidator

# Return a new invitation
# params = :assignment_id, :to_id, :from_id, :reply_status
def self.invitation_factory(params)
Invitation.new(params)
end

# check if the user is invited
def self.invited?(from_id, to_id, assignment_id)
conditions = {
to_id:,
from_id:,
assignment_id:,
reply_status: InvitationValidator::WAITING_STATUS
}
@invitations_exist = Invitation.where(conditions).exists?
end

# send invite email
def send_invite_email
InvitationSentMailer.with(invitation: self)
.send_invitation_email
.deliver_later
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

# This method handles all that needs to be done upon a user accepting an invitation.
# 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.
# 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: 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: InvitationValidator::REJECT_STATUS)
end

# This method handles all that need to be done upon an invitation retraction.
def retract_invitation(_logged_in_user)
destroy
end

# This will override the default as_json method in the ApplicationRecord class and specify
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
self.reply_status ||= InvitationValidator::WAITING_STATUS
end
end
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
50 changes: 50 additions & 0 deletions app/validators/invitation_validator.rb
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions app/views/invitation_sent_mailer/send_invitation_email.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<html>
<head>
<title>Invite from Expertiza</title>
</head>

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

3 changes: 2 additions & 1 deletion config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
get ':id/managed', on: :collection, action: :managed_users
end
resources :assignments
resources :invitations do
get 'user/:user_id/assignment/:assignment_id/', on: :collection, action: :invitations_for_user_assignment
end
end
end
end
15 changes: 15 additions & 0 deletions db/migrate/20230412020156_create_invitations.rb
Original file line number Diff line number Diff line change
@@ -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
28 changes: 20 additions & 8 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.