-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #243 from GSA/200_manage_evaluators_list
[200] Build Manage Evaluators List
- Loading branch information
Showing
22 changed files
with
1,056 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# frozen_string_literal: true | ||
|
||
class EvaluatorsController < ApplicationController | ||
before_action -> { authorize_user('challenge_manager') } | ||
|
||
# All routes are scoped to a Challenge Phase | ||
before_action :set_challenge_phase | ||
|
||
def index | ||
@evaluator_invitations = @phase.evaluator_invitations | ||
@existing_evaluators = @phase.evaluators | ||
end | ||
|
||
def create | ||
result = evaluator_service.process_evaluator_invitation( | ||
evaluator_invitation_params[:email], | ||
evaluator_invitation_params | ||
) | ||
|
||
if result[:success] | ||
redirect_to phase_evaluators_path(@phase), notice: result[:message] | ||
else | ||
flash.now[:alert] = result[:message] | ||
@evaluator_invitations = @phase.evaluator_invitations | ||
@existing_evaluators = @phase.evaluators | ||
render :index | ||
end | ||
end | ||
|
||
def destroy | ||
result = evaluator_service.remove_evaluator(params[:evaluator_type], params[:id]) | ||
|
||
if result[:success] | ||
flash[:notice] = result[:message] | ||
render json: { success: true, message: result[:message] } | ||
else | ||
render json: { success: false, message: result[:message] }, status: :unprocessable_entity | ||
end | ||
end | ||
|
||
def resend_invite | ||
@evaluator_invitation = @phase.evaluator_invitations.find(params[:id]) | ||
result = evaluator_service.resend_invitation(@evaluator_invitation) | ||
if result[:success] | ||
redirect_to phase_evaluators_path(@phase), | ||
notice: t('.success') | ||
else | ||
redirect_to phase_evaluators_path(@phase), | ||
alert: t('.failure') | ||
end | ||
end | ||
|
||
private | ||
|
||
def set_challenge_phase | ||
@phase = Phase.where(challenge: current_user.challenge_manager_challenges).find(params[:phase_id]) | ||
@challenge = @phase.challenge | ||
end | ||
|
||
def evaluator_service | ||
@evaluator_service ||= EvaluatorManagementService.new(@challenge, @phase) | ||
end | ||
|
||
def evaluator_invitation_params | ||
params.require(:evaluator_invitation).permit( | ||
:first_name, :last_name, :email, :challenge_id, :phase_id, :last_invite_sent | ||
) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# frozen_string_literal: true | ||
|
||
module EvaluatorsHelper | ||
def user_status(evaluator) | ||
if evaluator.is_a?(User) | ||
evaluator.status == 'active' ? "Available" : "Awaiting Approval" | ||
else | ||
"Invite Sent" | ||
end | ||
end | ||
|
||
def assigned_submissions_count(evaluator, challenge, phase) | ||
if evaluator.is_a?(User) | ||
evaluator.evaluator_submission_assignments. | ||
joins(:submission). | ||
where(submissions: { challenge:, phase: }). | ||
count | ||
else | ||
0 | ||
end | ||
end | ||
end |
6 changes: 1 addition & 5 deletions
6
app/helpers/manage_submissions_helper.rb → app/helpers/submissions_helper.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,11 @@ | ||
# frozen_string_literal: true | ||
|
||
module ManageSubmissionsHelper | ||
module SubmissionsHelper | ||
def eligible_for_evaluation?(submission) | ||
submission.judging_status.in?(%w[selected winner]) | ||
end | ||
|
||
def selected_to_advance?(submission) | ||
submission.judging_status.in?(%w[winner]) | ||
end | ||
|
||
def phase_has_recused_evaluator?(phase) | ||
phase.evaluator_submission_assignments.recused.exists? | ||
end | ||
end |
64 changes: 64 additions & 0 deletions
64
app/javascript/controllers/delete_evaluator_modal_controller.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { Controller } from "@hotwired/stimulus" | ||
|
||
export default class extends Controller { | ||
static targets = ["modal"] | ||
static values = { | ||
challengeId: String, | ||
evaluatorId: String, | ||
evaluatorType: String, | ||
phaseId: String | ||
} | ||
|
||
connect() { | ||
this.modalTarget.addEventListener('click', this.handleOutsideClick.bind(this)) | ||
} | ||
|
||
disconnect() { | ||
this.modalTarget.removeEventListener('click', this.handleOutsideClick.bind(this)) | ||
} | ||
|
||
open(event) { | ||
event.preventDefault() | ||
this.evaluatorIdValue = event.currentTarget.dataset.evaluatorId | ||
this.evaluatorTypeValue = event.currentTarget.dataset.evaluatorType | ||
this.phaseIdValue = event.currentTarget.dataset.phaseId | ||
this.modalTarget.showModal() | ||
} | ||
|
||
close() { | ||
this.modalTarget.close() | ||
} | ||
|
||
handleOutsideClick(event) { | ||
if (event.target === this.modalTarget) { | ||
this.close() | ||
} | ||
} | ||
|
||
confirm() { | ||
this.deleteEvaluator() | ||
} | ||
|
||
deleteEvaluator(forceDelete = false) { | ||
const csrfToken = document.querySelector('meta[name="csrf-token"]').content | ||
|
||
fetch(`/phases/${this.phaseIdValue}/evaluators/${this.evaluatorIdValue}`, { | ||
method: 'DELETE', | ||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, | ||
body: JSON.stringify({ evaluator_type: this.evaluatorTypeValue, phase_id: this.phaseIdValue, force_delete: forceDelete }) | ||
}) | ||
.then(response => { | ||
if (!response.ok) { throw new Error('Network response was not ok') } | ||
return response.json() | ||
}) | ||
.then(data => { | ||
if (data.success) { | ||
this.close() | ||
window.location.reload() | ||
} else { | ||
throw new Error(data.message || 'Failed to remove evaluator') | ||
} | ||
}) | ||
.catch(error => { alert(error.message || 'An error occurred while removing the evaluator') }) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
# frozen_string_literal: true | ||
|
||
class EvaluatorManagementService | ||
def initialize(challenge, phase) | ||
@challenge = challenge | ||
@phase = phase | ||
end | ||
|
||
def process_evaluator_invitation(email, invitation_params) | ||
user = User.find_by(email:) | ||
user ? add_existing_user_as_evaluator(user) : handle_invitation(email, invitation_params) | ||
end | ||
|
||
def remove_evaluator(evaluator_type, evaluator_id) | ||
case evaluator_type | ||
when 'user' | ||
remove_user_evaluator(evaluator_id) | ||
when 'invitation' | ||
remove_evaluator_invitation(evaluator_id) | ||
else | ||
{ success: false, message: 'Invalid evaluator type' } | ||
end | ||
end | ||
|
||
def self.accept_evaluator_invitation(user) | ||
invitations = EvaluatorInvitation.where(email: user.email) | ||
invitations.each do |invite| | ||
ChallengePhasesEvaluator.create(challenge: invite.challenge, phase: invite.phase, user:) | ||
invite.destroy | ||
end | ||
{ success: true, message: I18n.t('evaluators.accept_evaluator_invitation.success') } | ||
end | ||
|
||
# TODO: Implement sending the actual invitation email here | ||
def resend_invitation(invitation) | ||
if invitation.update(last_invite_sent: Time.current) | ||
{ success: true, | ||
message: I18n.t('evaluators.process_evaluator_invitation.invitation_resent', email: invitation.email) } | ||
else | ||
{ success: false, message: I18n.t('evaluators.resend_invite.failure') } | ||
end | ||
end | ||
|
||
private | ||
|
||
def add_existing_user_as_evaluator(user) | ||
if @phase.evaluators.include?(user) | ||
return { | ||
success: true, | ||
message: I18n.t('evaluators.process_evaluator_invitation.already_added', | ||
email: user.email) | ||
} | ||
end | ||
|
||
unless User::VALID_EVALUATOR_ROLES.include?(user.role) | ||
return { | ||
success: false, | ||
message: I18n.t('evaluators.process_evaluator_invitation.invalid_role', | ||
email: user.email) | ||
} | ||
end | ||
|
||
cpe = ChallengePhasesEvaluator.find_or_create_by(challenge: @challenge, phase: @phase, user:) | ||
|
||
if cpe.persisted? | ||
{ | ||
success: true, | ||
message: I18n.t('evaluators.process_evaluator_invitation.add_success', | ||
email: user.email) | ||
} | ||
else | ||
{ | ||
success: false, | ||
message: I18n.t('evaluators.process_evaluator_invitation.add_failure', | ||
email: user.email) | ||
} | ||
end | ||
end | ||
|
||
def handle_invitation(email, invitation_params) | ||
existing_invitation = @challenge.evaluator_invitations.find_by(email:, phase: @phase) | ||
existing_invitation ? resend_invitation(existing_invitation) : create_new_invitation(invitation_params) | ||
end | ||
|
||
def create_new_invitation(invitation_params) | ||
invitation = @challenge.evaluator_invitations.new( | ||
invitation_params.merge( | ||
phase: @phase, | ||
last_invite_sent: Time.current | ||
) | ||
) | ||
if invitation.save | ||
{ | ||
success: true, | ||
message: I18n.t( | ||
'evaluators.process_evaluator_invitation.invitation_sent', | ||
email: invitation_params[:email] | ||
) | ||
} | ||
else | ||
{ | ||
success: false, | ||
message: invitation.errors.full_messages.join(", ") | ||
} | ||
end | ||
end | ||
|
||
def remove_user_evaluator(evaluator_id) | ||
evaluator = User.find(evaluator_id) | ||
cpe = ChallengePhasesEvaluator.find_by(challenge: @challenge, phase: @phase, user: evaluator) | ||
if cpe.destroy | ||
{ success: true, message: I18n.t('evaluators.remove_user_evaluator.success') } | ||
else | ||
{ success: false, message: I18n.t('evaluators.remove_user_evaluator.failure') } | ||
end | ||
rescue ActiveRecord::RecordNotFound | ||
{ success: false, message: I18n.t('evaluators.remove_user_evaluator.evaluator_not_found') } | ||
rescue StandardError => e | ||
{ success: false, message: "Error: #{e.message}" } | ||
end | ||
|
||
def remove_evaluator_invitation(invitation_id) | ||
invitation = @challenge.evaluator_invitations.find_by!(id: invitation_id, phase: @phase) | ||
if invitation.destroy | ||
{ success: true, message: I18n.t('evaluators.remove_evaluator_invitation.success') } | ||
else | ||
{ success: false, message: I18n.t('evaluators.remove_evaluator_invitation.failure') } | ||
end | ||
rescue ActiveRecord::RecordNotFound | ||
{ success: false, message: I18n.t('evaluators.remove_evaluator_invitation.invitation_not_found') } | ||
rescue StandardError => e | ||
{ success: false, message: "Error: #{e.message}" } | ||
end | ||
end |
Oops, something went wrong.