Skip to content

Commit

Permalink
Merge branch '249/protect-eval-form-after-start-date' of github.com:G…
Browse files Browse the repository at this point in the history
…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
cpreisinger committed Dec 10, 2024
2 parents f5a86b9 + 449aeb1 commit 3df512f
Show file tree
Hide file tree
Showing 25 changed files with 823 additions and 64 deletions.
3 changes: 3 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ plugins:
rubocop:
enabled: true
channel: rubocop-1-56-3
checks:
Rubocop/Rails/ActionControllerFlashBeforeRender:
enabled: false
exclude_patterns:
- .nix-bundler
- config/
Expand Down
115 changes: 115 additions & 0 deletions app/controllers/evaluator_submission_assignments_controller.rb
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
48 changes: 35 additions & 13 deletions app/helpers/evaluators_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,44 @@

# View helpers for rendering users with the evaluator role.
module EvaluatorsHelper
STATUS_COLORS = {
not_started: 'bg-error-dark',
in_progress: 'bg-accent-warm-dark',
completed: 'bg-success-dark',
recused: 'bg-base',
unassigned: 'bg-base',
recused_unassigned: 'bg-base'
}.freeze

def user_status(evaluator)
if evaluator.is_a?(User)
evaluator.status == 'active' ? "Available" : "Awaiting Approval"
else
"Invite Sent"
end
return "Invite Sent" unless evaluator.is_a?(User)

evaluator.status == 'active' ? "Available" : "Awaiting Approval"
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
return 0 unless evaluator.is_a?(User)

evaluator.evaluator_submission_assignments.
joins(:submission).
where(submissions: { challenge:, phase: }).
where(status: :assigned).
count
end

def evaluation_submission_assignment_color(assignment)
status = if assignment.is_a?(EvaluatorSubmissionAssignment)
assignment.evaluation_status
else
assignment.to_sym
end

STATUS_COLORS[status]
end

def display_score(assignment)
return 'N/A' unless assignment.evaluation_status == :completed

assignment.evaluation&.total_score || 'N/A'
end
end
12 changes: 12 additions & 0 deletions app/javascript/controllers/evaluation_criteria_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ export default class extends Controller {
});
}

checkPointsOrWeightMax(event) {
const input = event.target;
const min = parseInt(input.min);
const max = parseInt(input.max);
const value = parseInt(input.value);

// If invalid value is entered then pop up error message
if (value && (value < min || value > max)) {
event.target.reportValidity();
}
}

updateScoringOptions(row, scoringType) {
const options = {
scaleOptions: row.querySelector(".criteria-scale-options"),
Expand Down
81 changes: 56 additions & 25 deletions app/javascript/controllers/evaluation_form_controller.js
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;
}
}
3 changes: 3 additions & 0 deletions app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ application.register("evaluation-criteria", EvaluationCriteriaController);

import DeleteEvaluatorModalController from "./delete_evaluator_modal_controller"
application.register("delete-evaluator-modal", DeleteEvaluatorModalController)

import UnassignEvaluatorSubmissionModalController from "./unassign_evaluator_submission_modal_controller"
application.register("unassign-evaluator-submission-modal", UnassignEvaluatorSubmissionModalController)
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');
});
}
}
Loading

0 comments on commit 3df512f

Please sign in to comment.