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

[82] Submissions List View for Evaluators #319

Merged
merged 31 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8919db1
88 | Move helper methods for evaluations
emmabjj Dec 11, 2024
42b088b
88 | Add submissions to evaluation controller
emmabjj Dec 11, 2024
8c888a0
88 | Make assignment stats a partial
emmabjj Dec 11, 2024
5c01125
88 | Add submissions route for evaluations
emmabjj Dec 11, 2024
5fadecb
88 | Update evaluator score format and status color
emmabjj Dec 11, 2024
c9fe4d4
88 | Frontend for evaluator's evaluations phases view
emmabjj Dec 11, 2024
e83b3e7
88 | Frontend for evaluator's assigned submissions
emmabjj Dec 11, 2024
c8156c5
88 | Restructure average & evaluator score methods
emmabjj Dec 11, 2024
eefa949
Merge branch 'dev' into 88_evaluators_evaluation_progress_summary
emmabjj Dec 11, 2024
1169888
88 | delete unused line
emmabjj Dec 11, 2024
d9663d9
Merge branch 'dev' into 88_evaluators_evaluation_progress_summary
emmabjj Dec 12, 2024
3832cf1
82 | Update scoring tests
emmabjj Dec 12, 2024
3fa4f70
82 | Fallback for closing date
emmabjj Dec 12, 2024
c4c78e3
Adjust copy
emmabjj Dec 20, 2024
2289946
Adjust helpers call
emmabjj Dec 20, 2024
f0c1bf4
Adjust stats styling on mobile
emmabjj Dec 20, 2024
9e875e2
Adjust score format
emmabjj Dec 20, 2024
7f6cc70
Update copy & tests
emmabjj Dec 20, 2024
f22a1dc
Merge branch 'dev' into 88_evaluators_evaluation_progress_summary
emmabjj Dec 20, 2024
e06aa2f
Merge branch 'dev' into 88_evaluators_evaluation_progress_summary
emmabjj Dec 20, 2024
2781ee7
Merge branch 'dev' into 88_evaluators_evaluation_progress_summary
emmabjj Jan 7, 2025
14b458d
82 | Exclude recuse in assignment status counts and show avg score af…
emmabjj Jan 7, 2025
591872b
82 | Update tests
emmabjj Jan 7, 2025
3c60b80
Merge branch 'dev' into 88_evaluators_evaluation_progress_summary
emmabjj Jan 8, 2025
105a42d
82 | Skip bullet - avoid unoptimized query warning
emmabjj Jan 8, 2025
94a98e6
82 | Update name of status color helper in view
emmabjj Jan 8, 2025
25a063e
Merge branch 'dev' into 88_evaluators_evaluation_progress_summary
emmabjj Jan 8, 2025
c1432fc
spec refacor, failing spec
stepchud Jan 8, 2025
c163d11
82 | Scope query for completed and assigned evaluations
emmabjj Jan 8, 2025
6621d4e
82 | Add test to assert evaluator access to unrelated phases is denied
emmabjj Jan 10, 2025
1abb4eb
Merge branch 'dev' into 88_evaluators_evaluation_progress_summary
emmabjj Jan 10, 2025
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
30 changes: 29 additions & 1 deletion app/controllers/evaluations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@

# Controller for evaluations CRUD actions.
class EvaluationsController < ApplicationController
include EvaluationsHelper
stepchud marked this conversation as resolved.
Show resolved Hide resolved

before_action -> { authorize_user('evaluator') }
def index; end
before_action :set_phase, except: [:index]

def index
@challenges = Challenge.joins(phases: :challenge_phases_evaluators).
where(challenge_phases_evaluators: { user_id: current_user.id }).
includes(phases: [:evaluation_form]).
distinct
end

def submissions
@assigned_submissions = @phase.evaluator_submission_assignments.
where(evaluator: current_user).
where(status: %i[assigned recused]).
includes(:submission, :evaluation).
ordered_by_status

@submissions_count = calculate_submissions_count(@assigned_submissions)
end

private

def set_phase
@phase = Phase.joins(:challenge_phases_evaluators).
where(challenge_phases_evaluators: { user_id: current_user.id }).
find(params[:id])
@challenge = @phase.challenge
end
end
26 changes: 2 additions & 24 deletions app/controllers/evaluator_submission_assignments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# Controller for evaluator submissions assignments index and update status
class EvaluatorSubmissionAssignmentsController < ApplicationController
include EvaluationsHelper
stepchud marked this conversation as resolved.
Show resolved Hide resolved

before_action -> { authorize_user('challenge_manager') }
before_action :set_challenge_and_phase
before_action :set_evaluator, only: [:index]
Expand Down Expand Up @@ -88,28 +90,4 @@ def redirect_to_assignment_path
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
2 changes: 1 addition & 1 deletion app/helpers/dashboard_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def dashboard_cards_by_role
],
evaluator: [
{ image_path: 'star_half', href: 'evaluations',
alt: 'submissions and evaluations', title: 'Submissions & Evaluations',
alt: 'submissions and evaluations', title: 'Evaluations',
subtitle: 'View submissions assigned to me and provide evaluations.' },
stepchud marked this conversation as resolved.
Show resolved Hide resolved
{ image_path: 'support', href: 'https://www.challenge.gov/cm-user-guide/',
alt: 'resources', title: 'Resources', subtitle: 'Learn how to make the most of the platform.' }
Expand Down
107 changes: 107 additions & 0 deletions app/helpers/evaluations_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# frozen_string_literal: true

# View helpers for calculating evaluation & submission details.
module EvaluationsHelper
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

Score = Struct.new(:raw_score, :formatted_score, :display_score)

def evaluation_submission_assignment_status_color(assignment)
STATUS_COLORS[assignment.evaluation_status]
end

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

assignment.evaluation&.total_score || 'N/A'
end

# individual evaluator score
def evaluator_score(assignment)
score = assignment.evaluation&.total_score

unless assignment.evaluation_status == :completed && score
return Score.new(0, "0", "N/A")
end

Score.new(score, score.to_s, score)
end

def average_score(submission)
completed_evaluations = submission.evaluations.where.not(completed_at: nil)

unless completed_evaluations.any?
return Score.new(0, "0", "N/A")
end
stepchud marked this conversation as resolved.
Show resolved Hide resolved

avg = completed_evaluations.average(:total_score)
score = avg ? avg.round : 0
Score.new(score, "#{score}%", "#{score}%")
stepchud marked this conversation as resolved.
Show resolved Hide resolved
end

# counting submissions & evaluations
def assigned_submissions_count(evaluator, challenge, phase)
return 0 unless evaluator.is_a?(User)

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

def remaining_evaluations_count(evaluator, challenge, phase)
return 0 unless evaluator.is_a?(User)

evaluator.evaluator_submission_assignments.
joins(:submission).
where(submissions: { challenge:, phase: }).
where(status: [:assigned, :recused]).
left_joins(:evaluation).
where('evaluations.completed_at IS NULL OR evaluations.id IS NULL').
count
end

def calculate_submissions_count(assignments)
counts = count_by_status(assignments)
counts.merge("total" => calculate_total(counts))
end

private

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
stepchud marked this conversation as resolved.
Show resolved Hide resolved

def count_completed(assignments)
assignments.count { |a| a.evaluation&.completed_at.present? }
end

def count_in_progress(assignments)
assignments.count { |a| a.evaluation.present? && a.evaluation.completed_at.nil? }
end

def count_not_started(assignments)
assignments.count { |a| a.assigned? && a.evaluation.nil? }
end

def count_recused(assignments)
assignments.count(&:recused?)
end

def calculate_total(counts)
counts.values.sum
end
end
35 changes: 0 additions & 35 deletions app/helpers/evaluators_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,9 @@

# 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)
return "Invite Sent" unless evaluator.is_a?(User)

evaluator.status == 'active' ? "Available" : "Awaiting Approval"
end

def assigned_submissions_count(evaluator, challenge, phase)
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
3 changes: 2 additions & 1 deletion app/models/evaluator_submission_assignment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ class EvaluatorSubmissionAssignment < ApplicationRecord
}

def self.ordered_by_status
select('evaluator_submission_assignments.*, evaluations.id AS evaluation_id, evaluations.completed_at').
includes(:evaluation).
select('evaluator_submission_assignments.*, evaluations.id AS evaluation_id, evaluations.completed_at').
left_joins(:evaluation).
to_a.
sort_by { |assignment| ORDER_VALUES[assignment.evaluation_status] }
Expand Down
44 changes: 44 additions & 0 deletions app/views/evaluations/_phases_table.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<table class="usa-table usa-table--stacked-header usa-table--borderless width-full gray-header margin-bottom-10">
<thead>
<tr>
<th scope="col">Challenge</th>
<th scope="col">Assigned to me</th>
<th scope="col">Remaining to evaluate</th>
<th scope="col">Due by</th>
<th scope="col"><span class="usa-sr-only">Evaluate</span></th>
</tr>
</thead>
<tbody>
<% @challenges.each do |challenge| %>
<% challenge.phases.each do |phase| %>
<tr>
<th data-label="Challenge Title" scope="row" class="text-top">
<%= challenge_phase_title(challenge, phase) %>
</th>
<td data-label="Assigned to me" class="text-top">
<%= assigned_submissions_count(current_user, challenge, phase) %>
</td>
<td data-label="Remaining to evaluate" class="text-top">
<%= remaining_evaluations_count(current_user, challenge, phase) %>
</td>
<td data-label="Due by" class="text-top">
<% if phase.evaluation_form %>
<%= phase.evaluation_form.closing_date %>
<% else %>
N/A
<% end %>
</td>
<td>
<div class="display-flex flex-column grid-row grid-gap-1 padding-bottom-2">
<%= link_to submissions_evaluation_path(phase), class: "usa-button font-body-2xs text-no-wrap width-full" do %>
Evaluate
stepchud marked this conversation as resolved.
Show resolved Hide resolved
<% end %>
</div>
</div>
</td>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
44 changes: 44 additions & 0 deletions app/views/evaluations/_submissions_table.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<%= render 'shared/assignment_stats', heading: 'Total Submissions' %>

<table class="usa-table usa-table--stacked-header usa-table--borderless width-full gray-header margin-bottom-10">
<thead>
<tr>
<th scope="col">Submission ID</th>
<th scope="col">My evaluation status</th>
<th scope="col">My score</th>
<th scope="col">Average Score</th>
<th scope="col"><span class="usa-sr-only">Evaluate</span></th>
</tr>
</thead>
<tbody>
<% @assigned_submissions.each do |assignment| %>
<tr>
<th data-label="Submission ID" scope="row">
<span class="mobile-lg:display-none">Submission ID</span><%= assignment.submission.id %>
</th>
<td data-label="My evaluation status">
<div class="display-flex flex-align-center">
<span class="usa-tag padding-x-1 <%= evaluation_submission_assignment_status_color(assignment) %>">
<%= assignment.evaluation_status.to_s.titleize %>
</span>
</div>
</td>
<td data-label="My score">
<div class="display-flex flex-align-center">
<%= evaluator_score(assignment).formatted_score %>
</div>
</td>
<td data-label="Average Score">
<%= average_score(assignment.submission).display_score %>
</td>
<td>
<div class="display-flex flex-no-wrap grid-row grid-gap-1">
<button class="usa-button font-body-2xs width-full text-no-wrap">
Evaluate
</button>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
13 changes: 8 additions & 5 deletions app/views/evaluations/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<div class="usa-card__container col-md-6">
<div class="usa-card__body">
<h1>Evaluations</h1>
<h1>Evaluations</h1>
<p class="text-normal padding-bottom-2">View challenges with submissions assigned to me to evaluate.</p>

<p>Find me in app/views/evaluations/index.html.erb</p>
<% if @challenges.empty? %>
<div class="text-normal">
<p>You currently do not have any challenges.</p>
</div>
</div>
<% else %>
<%= render partial: "phases_table" %>
<% end %>
12 changes: 12 additions & 0 deletions app/views/evaluations/submissions.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<%= render 'shared/back_link', path: evaluations_path %>

<h1><%= challenge_phase_title(@phase.challenge, @phase) %></h1>
<p class="text-normal">View challenge submissions and manage evaluation progress.</p>
stepchud marked this conversation as resolved.
Show resolved Hide resolved

<% if @assigned_submissions.empty? %>
<div class="text-normal">
<p>This challenge phase does not currently have any submissions.</p>
</div>
<% else %>
<%= render partial: "submissions_table" %>
<% end %>
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<%= assignment.submission.id %>
</td>
<td data-label="Evaluation status">
<span class="text-top usa-tag <%= evaluation_submission_assignment_color(assignment) %>"><%= assignment.evaluation_status.to_s.titleize %></span>
<span class="text-top usa-tag <%= evaluation_submission_assignment_status_color(assignment) %>"><%= assignment.evaluation_status.to_s.titleize %></span>
</td>
<td data-label="Score" class="text-top text-normal"><%= display_score(assignment) %></td>
<td data-label="Score" class="text-top text-normal"><%= evaluator_score(assignment).formatted_score %></td>
<td data-label="">
<div class="display-flex flex-justify-end">
<% if assignment.evaluation_status == :completed %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
<dialog
id="unassign-evaluator-submission-modal"
data-unassign-evaluator-submission-modal-target="modal"
aria-labelledby="modal-1-heading"
aria-describedby="modal-1-description"
aria-labelledby="unassign-modal-heading"
stepchud marked this conversation as resolved.
Show resolved Hide resolved
aria-describedby="unassign-modal-description"
class="radius-lg"
>
<div class="usa-modal__content maxw-mobile-lg">
<div class="usa-modal__main">
<h2 class="usa-modal__heading" id="modal-1-heading">
<h2 class="usa-modal__heading" id="unassign-modal-heading">
Are you sure you want to unassign an evaluator from this submission?
</h2>
<div class="usa-prose">
<p id="modal-1-description" data-unassign-evaluator-submission-modal-target="modalDescription">
<p id="unassign-modal-description" data-unassign-evaluator-submission-modal-target="modalDescription">
Unassigning an evaluator will delete any completed or in progress evaluations of this evaluator for this submission. Any other submissions that the evaluator is assigned will not be affected.
</p>
</div>
Expand Down
Loading
Loading