-
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 branch '249/protect-eval-form-after-start-date' of github.com:G…
…SA/Challenge_platform into 249/protect-eval-form-after-start-date * '249/protect-eval-form-after-start-date' of github.com:GSA/Challenge_platform: (53 commits) 249 Protect eval form updates after start_date [275-FIX] Fix for max input values for scale types (#288) Update app/views/evaluator_submission_assignments/_unassigned_submission_row.html.erb Update .codeclimate.yml rubocop name Disable rubocop check on flash before render quick syntax fix 179 | Adjust sorting scope and evaluation status quick syntax fix update status colors 179 | Adjust sorting scope and evaluation status Rename stat summary for evaluation submission assignments 179 | Add tests, update statuses, and colors 179 | Update flash, closing date, and error status 179 | Update tests for display scores 179 | Remove unused argument in display_score 179 | Update tests wip 179 | Update ordered by status query 179 | Update the display score to check for assignment completion 179 | Update js to use assignment 179 | Update route for evaluation submission assignments ...
- Loading branch information
Showing
25 changed files
with
823 additions
and
64 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
115 changes: 115 additions & 0 deletions
115
app/controllers/evaluator_submission_assignments_controller.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 |
---|---|---|
@@ -0,0 +1,115 @@ | ||
# frozen_string_literal: true | ||
|
||
# Controller for evaluator submissions assignments index and update status | ||
class EvaluatorSubmissionAssignmentsController < ApplicationController | ||
before_action -> { authorize_user('challenge_manager') } | ||
before_action :set_challenge_and_phase | ||
before_action :set_evaluator, only: [:index] | ||
before_action :set_assignment, only: [:update] | ||
|
||
def index | ||
@evaluator_assignments = @phase.evaluator_submission_assignments.includes(:submission).where(user_id: @evaluator.id) | ||
@assigned_submissions = @evaluator_assignments. | ||
where(status: %i[assigned recused]). | ||
includes(:evaluation). | ||
ordered_by_status | ||
@unassigned_submissions = @evaluator_assignments. | ||
where(status: %i[unassigned recused_unassigned]). | ||
ordered_by_status | ||
@submissions_count = calculate_submissions_count(@assigned_submissions) | ||
end | ||
|
||
# update only the status of the evaluation submission assignment to unassign or reassign an evaluator | ||
def update | ||
new_status = status_from_params | ||
|
||
unless valid_status?(new_status) | ||
return render_invalid_status_error | ||
end | ||
|
||
if update_assignment_status(new_status) | ||
handle_successful_update(new_status) | ||
else | ||
handle_failed_update(new_status) | ||
end | ||
end | ||
|
||
private | ||
|
||
def set_challenge_and_phase | ||
@phase = Phase.where(challenge: current_user.challenge_manager_challenges).find(params[:phase_id]) | ||
@challenge = @phase.challenge | ||
end | ||
|
||
def set_evaluator | ||
@evaluator = @phase.evaluators.find(params[:evaluator_id]) | ||
end | ||
|
||
def set_assignment | ||
@assignment = @phase.evaluator_submission_assignments.find(params[:id]) | ||
end | ||
|
||
def status_from_params | ||
status = params[:status] || params.dig(:evaluator_submission_assignment, :status) | ||
status&.to_sym | ||
end | ||
|
||
def valid_status?(status) | ||
EvaluatorSubmissionAssignment.statuses.keys.map(&:to_sym).include?(status) | ||
end | ||
|
||
def render_invalid_status_error | ||
render json: { success: false, message: 'Invalid status' }, status: :unprocessable_entity | ||
end | ||
|
||
def update_assignment_status(new_status) | ||
@assignment.update(status: new_status) | ||
end | ||
|
||
def handle_successful_update(new_status) | ||
flash[:success] = t("evaluator_submission_assignments.#{new_status}.success") | ||
respond_to do |format| | ||
format.html { redirect_to_assignment_path } | ||
format.json { render json: { success: true, message: flash[:success] } } | ||
end | ||
end | ||
|
||
def handle_failed_update(new_status) | ||
flash[:error] = t("evaluator_submission_assignments.#{new_status}.failure") | ||
respond_to do |format| | ||
format.html { redirect_to_assignment_path } | ||
format.json { render json: { success: false, message: flash[:error] }, status: :unprocessable_entity } | ||
end | ||
end | ||
|
||
def redirect_to_assignment_path | ||
redirect_to phase_evaluator_submission_assignments_path( | ||
@phase, | ||
evaluator_id: params[:evaluator_id] | ||
) | ||
end | ||
|
||
def calculate_submissions_count(assignments) | ||
counts = count_by_status(assignments) | ||
counts.merge("total" => calculate_total(counts)) | ||
end | ||
|
||
def count_by_status(assignments) | ||
{ | ||
"completed" => count_completed(assignments), | ||
"in_progress" => count_in_progress(assignments), | ||
"not_started" => count_not_started(assignments), | ||
"recused" => count_recused(assignments) | ||
} | ||
end | ||
|
||
def count_completed(assignments) = assignments.count { |a| a.evaluation&.completed_at.present? } | ||
|
||
def count_in_progress(assignments) = assignments.count { |a| a.evaluation.present? && a.evaluation.completed_at.nil? } | ||
|
||
def count_not_started(assignments) = assignments.count { |a| a.assigned? && a.evaluation.nil? } | ||
|
||
def count_recused(assignments) = assignments.count(&:recused?) | ||
|
||
def calculate_total(counts) = counts.values.sum | ||
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
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 |
---|---|---|
@@ -1,54 +1,85 @@ | ||
import { Controller } from "@hotwired/stimulus" | ||
import { Controller } from "@hotwired/stimulus"; | ||
|
||
// Connects to data-controller="evaluation-form" | ||
export default class extends Controller { | ||
static targets = ["challengeID", "phaseID", "startDate", "datePicker"]; | ||
|
||
handleChallengeSelect(e) { | ||
let id, phase_id, end_date | ||
[id, phase_id, end_date] = e.target.value.split(".") | ||
let id, phase_id, end_date; | ||
[id, phase_id, end_date] = e.target.value.split("."); | ||
if (id) { | ||
// set values of hidden form fields | ||
this.challengeIDTarget.value = id | ||
this.phaseIDTarget.value = phase_id | ||
// set values of hidden form fields | ||
this.challengeIDTarget.value = id; | ||
this.phaseIDTarget.value = phase_id; | ||
|
||
// set the start date of the evaluation form | ||
// set the start date of the evaluation form | ||
// to be the challenge's end date | ||
this.startDateTarget.innerHTML = end_date || "mm/dd/yyyy" | ||
let day, month, year | ||
[month, day, year] = end_date.split("/") | ||
this.datePickerTarget.setAttribute("data-min-date", `${year}-${month}-${day}`) | ||
this.startDateTarget.innerHTML = end_date || "mm/dd/yyyy"; | ||
let day, month, year; | ||
[month, day, year] = end_date.split("/"); | ||
this.datePickerTarget.setAttribute( | ||
"data-min-date", | ||
`${year}-${month}-${day}` | ||
); | ||
|
||
this.updateErrorMessage("evaluation_form_challenge_id", "") | ||
this.updateErrorMessage("evaluation_form_phase_id", "") | ||
this.updateErrorMessage("evaluation_form_challenge_id", ""); | ||
this.updateErrorMessage("evaluation_form_phase_id", ""); | ||
} else { | ||
this.updateErrorMessage("evaluation_form_challenge_id", "can't be blank") | ||
this.startDateTarget.innerHTML = "mm/dd/yyyy" | ||
this.updateErrorMessage("evaluation_form_challenge_id", "can't be blank"); | ||
this.startDateTarget.innerHTML = "mm/dd/yyyy"; | ||
} | ||
} | ||
|
||
// Opens all accordions, remove existing points/weights, update max points/weights values | ||
updateMaxPoints(e) { | ||
const form = e.target.closest('form[data-controller="evaluation-form"]'); | ||
const pointsWeights = form.querySelectorAll(".points-or-weight"); | ||
if (e.target.id == 'weighted_scale') { | ||
pointsWeights.forEach((input) => input.max = "100") | ||
} else { | ||
pointsWeights.forEach((input) => input.max = "9999") | ||
const weightedScale = e.target.value === "true"; | ||
|
||
if (weightedScale && this.hasValuesOverLimit(pointsWeights, 100)) { | ||
this.expandAllAccordions(form); | ||
} | ||
|
||
this.updateMaxValues(pointsWeights, weightedScale ? 100 : 9999); | ||
} | ||
|
||
// Helper: Check if any input values exceed a given limit | ||
hasValuesOverLimit(inputs, limit) { | ||
return Array.from(inputs).some( | ||
(input) => parseInt(input.value.trim()) > limit | ||
); | ||
} | ||
|
||
// Helper: Update max values for inputs | ||
updateMaxValues(inputs, maxValue) { | ||
inputs.forEach((input) => (input.max = maxValue)); | ||
Array.from(inputs).every((input) => { | ||
input.reportValidity(); | ||
}); | ||
} | ||
|
||
// Helper: Expand all accordions | ||
expandAllAccordions(form) { | ||
const accordionButtons = form.querySelectorAll(".usa-accordion__button"); | ||
const accordions = form.querySelectorAll(".usa-accordion__content"); | ||
|
||
accordionButtons.forEach((button) => | ||
button.setAttribute("aria-expanded", true) | ||
); | ||
accordions.forEach((content) => content.removeAttribute("hidden")); | ||
} | ||
|
||
validatePresence(e) { | ||
if (!e.target.value) { | ||
e.target.classList.add("border-secondary") | ||
this.updateErrorMessage(e.target.id, "can't be blank") | ||
|
||
e.target.classList.add("border-secondary"); | ||
this.updateErrorMessage(e.target.id, "can't be blank"); | ||
} else { | ||
e.target.classList.remove("border-secondary") | ||
this.updateErrorMessage(e.target.id, "") | ||
e.target.classList.remove("border-secondary"); | ||
this.updateErrorMessage(e.target.id, ""); | ||
} | ||
} | ||
|
||
updateErrorMessage(field, message) { | ||
document.getElementById(field + "_error").innerHTML = message | ||
document.getElementById(field + "_error").innerHTML = message; | ||
} | ||
} |
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
66 changes: 66 additions & 0 deletions
66
app/javascript/controllers/unassign_evaluator_submission_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,66 @@ | ||
import { Controller } from "@hotwired/stimulus" | ||
|
||
export default class extends Controller { | ||
static targets = ["modal"] | ||
static values = { | ||
phaseId: String, | ||
assignmentId: String | ||
} | ||
|
||
connect() { | ||
this.modalTarget.addEventListener('click', this.handleOutsideClick.bind(this)); | ||
} | ||
|
||
disconnect() { | ||
this.modalTarget.removeEventListener('click', this.handleOutsideClick.bind(this)); | ||
} | ||
|
||
open(event) { | ||
event.preventDefault(); | ||
this.setValues(event.currentTarget.dataset); | ||
this.modalTarget.showModal(); | ||
} | ||
|
||
close() { | ||
this.modalTarget.close(); | ||
} | ||
|
||
handleOutsideClick(event) { | ||
if (event.target === this.modalTarget) { | ||
this.close(); | ||
} | ||
} | ||
|
||
setValues(dataset) { | ||
this.assignmentIdValue = dataset.assignmentId; | ||
this.phaseIdValue = dataset.phaseId; | ||
} | ||
|
||
unassignEvaluatorSubmission() { | ||
const csrfToken = document.querySelector('meta[name="csrf-token"]').content; | ||
fetch(`/phases/${this.phaseIdValue}/evaluator_submission_assignments/${this.assignmentIdValue}`, { | ||
method: 'PATCH', | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'X-CSRF-Token': csrfToken, | ||
'Accept': 'application/json' | ||
}, | ||
body: JSON.stringify({ | ||
evaluator_submission_assignment: { status: 'unassigned' } | ||
}) | ||
}) | ||
.then(response => response.json()) | ||
.then(data => { | ||
if (data.success) { | ||
const evaluatorId = new URLSearchParams(window.location.search).get('evaluator_id'); | ||
window.location.href = `/phases/${this.phaseIdValue}/evaluator_submission_assignments?evaluator_id=${evaluatorId}`; | ||
} else { | ||
throw new Error(data.message || 'Failed to unassign evaluator from submission'); | ||
} | ||
}) | ||
.catch(error => { | ||
console.error('Error:', error); | ||
alert(error.message || 'An error occurred while unassigning the evaluator from the submission'); | ||
}); | ||
} | ||
} |
Oops, something went wrong.