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

[200] Build Manage Evaluators List #243

Merged
merged 56 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
589d27f
200 | Add frontend to manage evaluators list page
emmabjj Oct 29, 2024
8f3fa45
200 | Add delete modal to manage evaluators and functionality
emmabjj Oct 29, 2024
84f3b13
200 | Update header and add challenge status to manage submissions index
emmabjj Oct 29, 2024
12c95ff
200 | Add functionality to add/invite, delete, and resend evaluator +…
emmabjj Oct 29, 2024
89fedc4
200 | Center back link on delete evaluator modal
emmabjj Oct 29, 2024
4f48437
200 | Uniqueness message handled elsewhere
emmabjj Oct 29, 2024
3335e3f
200 | Add evaluator messages to locale
emmabjj Oct 29, 2024
b18a5af
200 | Indentation and lazy lookup for locale
emmabjj Oct 29, 2024
f13edf1
set node-version for circleci
emmabjj Oct 29, 2024
74ebbfc
200 | Break up logic into smaller methods
emmabjj Oct 29, 2024
25fc5b6
200 | Update call
emmabjj Oct 29, 2024
32dbc51
Merge branch 'dev' into 200_manage_evaluators_list
stepchud Oct 30, 2024
0453855
Update .circleci/config.yml
stepchud Oct 30, 2024
6bd9cf3
Merge branch 'dev' into 200_manage_evaluators_list
stepchud Oct 30, 2024
888d2ae
200 | Update queries and naming
emmabjj Nov 1, 2024
e4b10f7
200 | Use stimulus for delete evaluator modal
emmabjj Nov 1, 2024
2eaf379
Merge branch 'dev' into 200_manage_evaluators_list
emmabjj Nov 1, 2024
333a590
200 | Add missing bracket after merge conflict
emmabjj Nov 1, 2024
296a0fe
200 | Update query calls
emmabjj Nov 4, 2024
f25eb6d
200 | Update manage evaluators destroy route
emmabjj Nov 5, 2024
f587d15
[245] Invited Evaluators User Setup (#254)
emmabjj Nov 6, 2024
2a17abd
200 | Adjust challenge route
emmabjj Nov 6, 2024
219095e
200 | remove unused param
emmabjj Nov 6, 2024
db6e2a7
200 | Add guard clause
emmabjj Nov 6, 2024
ffb623d
200 | Remove redundant self check
emmabjj Nov 6, 2024
aaa1c69
Merge branch 'dev' into 200_manage_evaluators_list
emmabjj Nov 6, 2024
1764e7e
200 | Fix failing tests
emmabjj Nov 7, 2024
f77730f
consolidate routes
stepchud Nov 7, 2024
adc6422
revert structure.sql changes
stepchud Nov 7, 2024
98bb679
remove rbenv from .envrc
stepchud Nov 7, 2024
d23da73
200 | Remove unnecessary css property
emmabjj Nov 12, 2024
b9364aa
200 | Adjust UI
emmabjj Nov 12, 2024
b725416
200 | Move controller specs to spec/requests
emmabjj Nov 12, 2024
667cb62
200 | Require authorized user in evaluator invitations controller
emmabjj Nov 12, 2024
9f3d3af
200 | Move business logic to evaluator management service object
emmabjj Nov 12, 2024
c1cb69c
200 | Add evaluator management service object + tests
emmabjj Nov 12, 2024
a630bb9
200 | Use service object on after_create in user model for processign…
emmabjj Nov 12, 2024
5e7eb94
200 | Add translations
emmabjj Nov 12, 2024
de375d6
200 | Fix some formatting
emmabjj Nov 13, 2024
32e3205
scope the Challenge query to the current_user
stepchud Nov 13, 2024
0f60b7c
Merge remote-tracking branch 'origin/dev' into 200_manage_evaluators_…
stepchud Nov 15, 2024
a56ba86
review feedback
stepchud Nov 15, 2024
f15ffad
update javascript url
stepchud Nov 15, 2024
86c9b6f
fix n+1 queries
stepchud Nov 15, 2024
4fe4a1b
codeclimate shrink JS function
stepchud Nov 15, 2024
13a08bc
update the invitation routes
stepchud Nov 19, 2024
73af9de
missing status check
stepchud Nov 19, 2024
13dbc4c
rubocop specs
stepchud Nov 19, 2024
a30de58
Merge pull request #279 from GSA/200_manage_evaluators_restful
emmabjj Nov 19, 2024
15405e0
Merge branch 'dev' into 200_manage_evaluators_list
emmabjj Nov 19, 2024
7cecef3
Bullet failure: Remove unnecessary eager loaded :submissions not used…
emmabjj Nov 19, 2024
4064d4f
adjust error message
stepchud Nov 19, 2024
0df811a
inline another render
stepchud Nov 19, 2024
380c7ce
rename helper, remove duplicate helper method
stepchud Nov 19, 2024
b13b54f
Merge branch 'dev' into 200_manage_evaluators_list
stepchud Nov 19, 2024
e41dd2b
remove unnecessary code
stepchud Nov 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/assets/stylesheets/application.sass.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,11 @@ dialog::backdrop {
background-color: black;
border-color: black;
}

.usa-table.usa-table--borderless.gray-header {
stepchud marked this conversation as resolved.
Show resolved Hide resolved
thead {
th {
background-color: #dfe1e2 !important;
stepchud marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
27 changes: 27 additions & 0 deletions app/controllers/evaluator_invitations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

class EvaluatorInvitationsController < ApplicationController
before_action :set_challenge
before_action :set_evaluator_invitation

def resend_invitation
if @evaluator_invitation.update(last_invite_sent: Time.current)
# TODO: Implement sending the actual invitation email here
redirect_to challenge_manage_evaluators_path(@challenge),
notice: t('.success')
else
redirect_to challenge_manage_evaluators_path(@challenge),
alert: t('.failure')
end
end

private

def set_challenge
@challenge = Challenge.find(params[:challenge_id])
stepchud marked this conversation as resolved.
Show resolved Hide resolved
end

def set_evaluator_invitation
@evaluator_invitation = @challenge.evaluator_invitations.find(params[:id])
end
end
179 changes: 179 additions & 0 deletions app/controllers/manage_evaluators_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# frozen_string_literal: true

class ManageEvaluatorsController < ApplicationController
include ManageEvaluatorsHelper

before_action :set_challenge
stepchud marked this conversation as resolved.
Show resolved Hide resolved

VALID_EVALUATOR_ROLES = %w[evaluator solver challenge_manager].freeze

def index
@phases = @challenge.phases.order(:start_date)
stepchud marked this conversation as resolved.
Show resolved Hide resolved

if @phases.empty?
handle_empty_phases
stepchud marked this conversation as resolved.
Show resolved Hide resolved
else
handle_existing_phases
end
end

def create
@phase = @challenge.phases.find(evaluator_invitation_params[:phase_id])
result = process_evaluator_invitation(evaluator_invitation_params[:email])

if result[:success]
handle_successful_creation(result)
else
handle_failed_creation
end
end

def destroy
@phase = @challenge.phases.find(params[:phase_id])
result = process_evaluator_removal(params[:evaluator_type], params[:evaluator_id])
stepchud marked this conversation as resolved.
Show resolved Hide resolved

render_json_response(result)
end

private

# Setup methods
def set_challenge
@challenge = Challenge.find(params[:challenge_id])
stepchud marked this conversation as resolved.
Show resolved Hide resolved
end

def evaluator_invitation_params
params.require(:evaluator_invitation).permit(
:first_name, :last_name, :email, :challenge_id, :phase_id, :last_invite_sent
)
end

# Index action helpers
def handle_empty_phases
flash.now[:alert] = t('.no_phases_alert')
@evaluator_invitations = []
@existing_evaluators = []
end

def handle_existing_phases
@phase = select_phase
@evaluator_invitations = fetch_evaluator_invitations
@existing_evaluators = fetch_existing_evaluators
end

def select_phase
params[:phase_id] ? @phases.find(params[:phase_id]) : @phases.first
end

def fetch_evaluator_invitations
@challenge.evaluator_invitations.where(phase: @phase)
stepchud marked this conversation as resolved.
Show resolved Hide resolved
end

def fetch_existing_evaluators
@challenge.evaluators.
joins(:challenge_phases_evaluators).
where(challenge_phases_evaluators: { phase: @phase }).
distinct
stepchud marked this conversation as resolved.
Show resolved Hide resolved
end

# Create action helpers
def process_evaluator_invitation(email)
existing_invitation = @challenge.evaluator_invitations.find_by(email:, phase: @phase)
stepchud marked this conversation as resolved.
Show resolved Hide resolved
user = User.find_by(email:)

if existing_invitation
resend_invitation(existing_invitation)
elsif user && valid_evaluator_role?(user)
add_user_as_evaluator(user)
else
create_new_invitation(email)
end
end

# prevent duplicate evaluator invitations
def resend_invitation(invitation)
invitation.update(last_invite_sent: Time.current) # only update last_invite_sent for now
{
success: true,
message: "An invitation to this challenge has already been sent to " \
"#{invitation.email}. Invitation has been resent."
}
end

def valid_evaluator_role?(user)
VALID_EVALUATOR_ROLES.include?(user.role)
end

def add_user_as_evaluator(user)
cpe = ChallengePhasesEvaluator.find_or_create_by(challenge: @challenge, phase: @phase, user:)
if cpe.persisted?
{ success: true, message: "#{user.email} has been added as an evaluator for this phase." }
else
{ success: false, message: "Failed to add #{user.email} as an evaluator." }
end
end

def create_new_invitation(email)
invitation = @challenge.evaluator_invitations.new(evaluator_invitation_params.merge(phase: @phase))
if invitation.save
{ success: true, message: "Invitation sent to #{email} for this challenge phase." }
else
{ success: false, message: invitation.errors.full_messages.join(", ") }
end
end

def handle_successful_creation(result)
redirect_to challenge_manage_evaluators_path(@challenge, phase_id: @phase.id),
notice: result[:message]
end

def handle_failed_creation
stepchud marked this conversation as resolved.
Show resolved Hide resolved
@evaluator_invitations = fetch_evaluator_invitations
@existing_evaluators = fetch_existing_evaluators
render :index
end

# Destroy action helpers
def process_evaluator_removal(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 remove_user_evaluator(evaluator_id)
evaluator = @challenge.evaluators.find_by(id: evaluator_id)
return { success: false, message: 'Evaluator not found' } unless evaluator
stepchud marked this conversation as resolved.
Show resolved Hide resolved

cpe = ChallengePhasesEvaluator.find_by(challenge: @challenge, phase: @phase, user: evaluator)
stepchud marked this conversation as resolved.
Show resolved Hide resolved
if cpe&.destroy
{ success: true, message: t('manage_evaluators.remove_user_evaluator.success') }
else
{ success: false, message: t('manage_evaluators.remove_user_evaluator.failure') }
end
rescue StandardError => e
{ success: false, message: "Error: #{e.message}" }
stepchud marked this conversation as resolved.
Show resolved Hide resolved
end

def remove_evaluator_invitation(invitation_id)
invitation = @challenge.evaluator_invitations.find_by(id: invitation_id, phase: @phase)
stepchud marked this conversation as resolved.
Show resolved Hide resolved
if invitation&.destroy
{ success: true, message: t('manage_evaluators.remove_evaluator_invitation.success') }
else
{ success: false, message: t('manage_evaluators.remove_evaluator_invitation.failure') }
end
end

def render_json_response(result)
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
end
32 changes: 32 additions & 0 deletions app/helpers/manage_evaluators_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module ManageEvaluatorsHelper
def user_status(evaluator, challenge)
if evaluator.is_a?(User)
user_status_for_existing_user(evaluator, challenge)
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

private

def user_status_for_existing_user(user, challenge)
if challenge.challenge_phases_evaluators.exists?(user_id: user.id)
stepchud marked this conversation as resolved.
Show resolved Hide resolved
user.status == 'active' ? "Available" : "Awaiting Approval"
else
"Invite Sent"
end
end
end
1 change: 1 addition & 0 deletions app/javascript/application.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import "./controllers"
import "./delete_evaluator_modal"
77 changes: 77 additions & 0 deletions app/javascript/delete_evaluator_modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
export function initializeDeleteEvaluatorModal() {
let currentEvaluatorId, currentEvaluatorType, challengeId, phaseId;

document.querySelectorAll('[data-open-modal]').forEach(button => {
stepchud marked this conversation as resolved.
Show resolved Hide resolved
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
currentEvaluatorId = button.dataset.evaluatorId;
currentEvaluatorType = button.dataset.evaluatorType;
challengeId = button.dataset.challengeId;
phaseId = button.dataset.phaseId;
const modal = document.getElementById('delete-evaluator-modal');
modal.classList.add('is-visible');
return false;
});
});

document.getElementById('confirmDelete').addEventListener('click', () => {
deleteEvaluator();
});

function deleteEvaluator(forceDelete = false) {
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
const challengeId = document.querySelector('[data-challenge-id]').dataset.challengeId;

fetch(`/challenges/${challengeId}/manage_evaluators`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
evaluator_id: currentEvaluatorId,
evaluator_type: currentEvaluatorType,
stepchud marked this conversation as resolved.
Show resolved Hide resolved
phase_id: phaseId,
force_delete: forceDelete
})
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.success) {
const modal = document.getElementById('delete-evaluator-modal');
modal.classList.remove('is-visible');
window.location.reload();
} else {
throw new Error(data.message || 'Failed to remove evaluator');
}
})
.catch(error => {
console.error('Error:', error);
alert(error.message || 'An error occurred while removing the evaluator');
});
}

document.querySelectorAll('[data-close-modal]').forEach(element => {
element.addEventListener('click', () => {
const modal = document.getElementById('delete-evaluator-modal');
modal.classList.remove('is-visible');

const modalDescription = document.getElementById('modal-1-description');
modalDescription.textContent = 'Deleting an evaluator from the challenge will remove the evaluator from any submissions of this challenge that the evaluator is assigned to. It will also delete any of their completed or in progress evaluations for those submissions.';

const confirmButton = document.getElementById('confirmDelete');
confirmButton.textContent = 'Yes';
confirmButton.onclick = () => deleteEvaluator();
});
});
}

if (document.querySelector('[data-page="manage-evaluators"]')) {
document.addEventListener('DOMContentLoaded', initializeDeleteEvaluatorModal);
}
46 changes: 46 additions & 0 deletions app/views/manage_evaluators/_delete_evaluator_modal.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<div
class="usa-modal"
id="delete-evaluator-modal"
aria-labelledby="modal-1-heading"
aria-describedby="modal-1-description"
>
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="modal-1-heading">
Are you sure you want to delete an evaluator from this challenge?
</h2>
<div class="usa-prose">
<p id="modal-1-description">
Deleting an evaluator from the challenge will remove the evaluator from any submissions of this challenge that the evaluator is assigned to. It will also delete any of their completed or in progress evaluations for those submissions.
</p>
</div>
<div class="usa-modal__footer">
<ul class="usa-button-group">
<li class="usa-button-group__item">
<button type="button" class="usa-button" id="confirmDelete">
Yes
</button>
</li>
<li class="usa-button-group__item padding-top-1 text-center">
<a
href="#"
class="usa-link"
data-close-modal
>
Back
</a>
</li>
</ul>
</div>
</div>
<button
type="button"
class="usa-button usa-modal__close"
aria-label="Close this window"
data-close-modal
>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="/assets/img/sprite.svg#close"></use>
</svg>
</button>
</div>
Loading
Loading