Skip to content

Commit

Permalink
Merge branch '237/eval-form-phase-uniqueness' of github.com:GSA/Chall…
Browse files Browse the repository at this point in the history
…enge_platform into 237/eval-form-phase-uniqueness

* '237/eval-form-phase-uniqueness' of github.com:GSA/Challenge_platform: (56 commits)
  237 Add unique phase constraint on eval forms
  Bump rubocop from 1.68.0 to 1.69.1
  Bump selenium-webdriver from 4.26.0 to 4.27.0
  [304] Collapsible Column Layout (#310)
  [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
  ...
  • Loading branch information
cpreisinger committed Dec 11, 2024
2 parents 1c0182c + 327e491 commit 58f8a20
Show file tree
Hide file tree
Showing 33 changed files with 916 additions and 83 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
22 changes: 12 additions & 10 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,11 @@ GEM
activesupport (>= 5.0.0)
jsbundling-rails (1.3.1)
railties (>= 6.0.0)
json (2.8.2)
json (2.9.0)
jwt (2.9.3)
base64
language_server-protocol (3.17.0.3)
logger (1.6.1)
logger (1.6.2)
loofah (2.23.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
Expand Down Expand Up @@ -267,7 +267,7 @@ GEM
rake (13.2.1)
rdoc (6.8.1)
psych (>= 4.0.0)
regexp_parser (2.9.2)
regexp_parser (2.9.3)
reline (0.5.11)
io-console (~> 0.5)
rexml (3.3.9)
Expand All @@ -290,17 +290,17 @@ GEM
rspec-support (3.13.1)
rspec_junit_formatter (0.6.0)
rspec-core (>= 2, < 4, != 2.12.0)
rubocop (1.68.0)
rubocop (1.69.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0)
rubocop-ast (>= 1.32.2, < 2.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.36.2, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.36.1)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.36.2)
parser (>= 3.3.1.0)
rubocop-capybara (2.21.0)
rubocop (~> 1.41)
Expand All @@ -319,7 +319,7 @@ GEM
ruby-progressbar (1.13.0)
rubyzip (2.3.2)
securerandom (0.3.2)
selenium-webdriver (4.26.0)
selenium-webdriver (4.27.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
Expand All @@ -341,7 +341,9 @@ GEM
railties (>= 6.0.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (2.6.0)
unicode-display_width (3.1.2)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uniform_notifier (1.16.0)
uri (1.0.2)
useragent (0.16.10)
Expand Down
4 changes: 4 additions & 0 deletions app/assets/uswds/_uswds-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ Add a list of changed settings in the form $setting: value.
filter: invert(99%) sepia(2%) saturate(3661%) hue-rotate(191deg) brightness(117%) contrast(80%);
}

.width-half {
width: 50%;
}

.usa-social-link {
background-color: white;
}
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;
}
}
Loading

0 comments on commit 58f8a20

Please sign in to comment.