Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c82ec68
Refactor judge selection input in judging pool for improved usability
rsmoke Jun 30, 2025
b5e4b87
Add judge lookup functionality for dynamic judge selection
rsmoke Jun 30, 2025
8dccdf9
Add user lookup functionality for dynamic user search
rsmoke Jun 30, 2025
dd01ab9
Enhance UID lookup controller for dynamic user search
rsmoke Jun 30, 2025
9b502b7
Refactor assignment form for improved UID lookup integration
rsmoke Jun 30, 2025
bacf8d6
Add button for sending judging instructions in judges management view
rsmoke Jun 30, 2025
68df3ec
Conditionally display the "Send judging instructions" button in judge…
rsmoke Jun 30, 2025
e6870fd
Update UID lookup integration in judging pool partial
rsmoke Jun 30, 2025
3c1037f
Add user and judge lookup routes for enhanced autocomplete functionality
rsmoke Jun 30, 2025
988b1d4
Add tests for judge_lookup action in JudgingAssignmentsController
rsmoke Jun 30, 2025
b2b979b
Add tests for user lookup action in UsersController
rsmoke Jun 30, 2025
eb5385d
Add send_instructions action in JudgingRoundsController
rsmoke Jul 1, 2025
c3410ce
Add JudgingInstructionsMailer for sending judging instructions
rsmoke Jul 1, 2025
5c64a57
Update contact email fallback in ResultsMailer
rsmoke Jul 1, 2025
1daef05
Add normalize_email method to User model
rsmoke Jul 1, 2025
6a97237
Conditionally render "Send judging instructions" button in judges man…
rsmoke Jul 1, 2025
9c831e5
Add send_instructions view for JudgingInstructionsMailer</message>
rsmoke Jul 1, 2025
066b841
Add text view for judging instructions in JudgingInstructionsMailer</…
rsmoke Jul 1, 2025
8564ebb
Add send_instructions route to JudgingRoundsController</message>
rsmoke Jul 1, 2025
d97588b
Configure ActionMailer default URL options in test environment
rsmoke Jul 1, 2025
7c26e90
Add tests for JudgingInstructionsMailer</message>
rsmoke Jul 1, 2025
5726315
Update default contact email in ResultsMailer tests
rsmoke Jul 1, 2025
cbe082c
Add preview for JudgingInstructionsMailer</message>
rsmoke Jul 1, 2025
5543bcc
Merge pull request #146 from lsa-mis/LRA-1095-evaluate-review-in-line…
rsmoke Jul 1, 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
17 changes: 17 additions & 0 deletions app/controllers/judging_assignments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ def create_judge
end
end

def judge_lookup
@container = Container.find(params[:container_id])
@contest_description = ContestDescription.find(params[:contest_description_id])
@contest_instance = ContestInstance.find(params[:contest_instance_id])

assigned_ids = @contest_instance.judging_assignments.pluck(:user_id)
query = params[:q].to_s.strip

available_judges = User.joins(:roles)
.where(roles: { kind: 'Judge' })
.where.not(id: assigned_ids)
.where('users.first_name LIKE :q OR users.last_name LIKE :q OR users.email LIKE :q', q: "%#{query}%")
.limit(10)

render json: available_judges.map { |u| { id: u.id, name: "#{u.first_name} #{u.last_name} (#{u.email})" } }
end

private

def set_contest_instance
Expand Down
34 changes: 33 additions & 1 deletion app/controllers/judging_rounds_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class JudgingRoundsController < ApplicationController
before_action :set_contest_instance
before_action :set_judging_round, only: [ :show, :edit, :update, :destroy, :activate, :deactivate, :complete, :uncomplete, :update_rankings, :finalize_rankings ]
before_action :set_judging_round, only: [ :show, :edit, :update, :destroy, :activate, :deactivate, :complete, :uncomplete, :update_rankings, :finalize_rankings, :send_instructions ]
before_action :authorize_contest_instance
before_action :check_edit_warning, only: [ :edit, :update ]

Expand Down Expand Up @@ -99,6 +99,38 @@ def uncomplete
end
end

def send_instructions
if @judging_round.judges.empty?
redirect_to container_contest_description_contest_instance_judging_assignments_path(
@container, @contest_description, @contest_instance
), alert: 'No judges assigned to this round.'
return
end

sent_count = 0
failed_emails = []

@judging_round.round_judge_assignments.active.includes(:user).each do |assignment|
begin
JudgingInstructionsMailer.send_instructions(assignment).deliver_later
sent_count += 1
rescue => e
failed_emails << assignment.user.email
Rails.logger.error "Failed to send judging instructions to #{assignment.user.email}: #{e.message}"
end
end

if failed_emails.empty?
notice_message = "Judging instructions sent successfully to #{sent_count} judge(s)."
else
notice_message = "Sent instructions to #{sent_count} judge(s). Failed to send to: #{failed_emails.join(', ')}"
end

redirect_to container_contest_description_contest_instance_judging_assignments_path(
@container, @contest_description, @contest_instance
), notice: notice_message
end

def update_rankings
rankings = params[:rankings]

Expand Down
7 changes: 7 additions & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class UsersController < ApplicationController
def lookup
query = params[:q].to_s.strip
users = User.where('first_name LIKE :q OR last_name LIKE :q OR email LIKE :q OR uid LIKE :q', q: "%#{query}%").limit(10)
render json: users.map { |u| { id: u.id, name: "#{u.first_name} #{u.last_name} (#{u.email})", uid: u.uid } }
end
end
12 changes: 7 additions & 5 deletions app/javascript/controllers/uid_lookup_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["displayInput", "uidInput", "results"]
static values = { url: String }

connect() {
this.timeout = null
Expand All @@ -13,7 +14,8 @@ export default class extends Controller {
this.timeout = setTimeout(() => {
const query = this.displayInputTarget.value.trim()
if (query.length > 0) {
fetch(`/containers/lookup_user?uid=${encodeURIComponent(query)}`)
const url = this.hasUrlValue ? this.urlValue : '/containers/lookup_user'
fetch(`${url}?${url.includes('lookup_user') ? 'uid' : 'q'}=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
this.showSuggestions(data)
Expand All @@ -28,11 +30,11 @@ export default class extends Controller {
this.clearSuggestions()
users.forEach(user => {
const option = document.createElement('div')
option.textContent = user.display_name_and_uid
option.textContent = user.display_name_and_uid || user.name || `${user.first_name} ${user.last_name} (${user.email})`
option.classList.add('suggestion')
option.addEventListener('click', () => {
this.displayInputTarget.value = user.display_name_and_uid
this.uidInputTarget.value = user.uid
this.displayInputTarget.value = user.display_name_and_uid || user.name || `${user.first_name} ${user.last_name} (${user.email})`
this.uidInputTarget.value = user.uid || user.id
this.clearSuggestions()
})
this.resultsTarget.appendChild(option)
Expand All @@ -49,4 +51,4 @@ export default class extends Controller {
alert('Please select a valid user.')
}
}
}
}
32 changes: 32 additions & 0 deletions app/mailers/judging_instructions_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class JudgingInstructionsMailer < ApplicationMailer
def send_instructions(round_judge_assignment)
@round_judge_assignment = round_judge_assignment
@judge = @round_judge_assignment.user
@judging_round = @round_judge_assignment.judging_round
@contest_instance = @judging_round.contest_instance
@contest_description = @contest_instance.contest_description
@container = @contest_description.container
@judge_email = @judge.normalize_email

# Get the special instructions from the judging round
@special_instructions = @judging_round.special_instructions.presence

# Get the contact email for display in the email body
# Use container's contact_email if present, otherwise use the default reply-to email
@contact_email = @container.contact_email.presence || 'LSA Evaluate Support <lsa-evaluate-support@umich.edu>'

subject = "Judging Instructions for #{@contest_description.name} - Round #{@judging_round.round_number}"

# Set mail options
mail_options = {
to: @judge_email,
subject: subject
}

# Override reply_to with container's contact_email if present
# If not present, the default reply-to from ApplicationMailer will be used
mail_options[:reply_to] = @container.contact_email if @container.contact_email.present?

mail(mail_options)
end
end
5 changes: 1 addition & 4 deletions app/mailers/results_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ def entry_evaluation_notification(entry, round)
@container = @contest_description.container

# Get the contact email from the container, with fallbacks if not present
@contact_email = @container.contact_email.presence ||
Rails.application.credentials.dig(:mailer, :default_contact_email) ||
Rails.application.credentials.dig(:devise, :mailer_sender) ||
'contests@example.com'
@contact_email = @container.contact_email.presence || 'LSA Evaluate Support <lsa-evaluate-support@umich.edu>'

# Get all rankings for this entry in this round
@rankings = EntryRanking.where(entry: @entry, judging_round: @round)
Expand Down
10 changes: 10 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ def display_name_or_first_name_last_name
(display_name.presence || "#{first_name} #{last_name}")
end

def normalize_email
if email.end_with?('@umich.edu') && email.include?('+')
local_part, _domain = email.split('@')
username, original_domain = local_part.split('+')
"#{username}@#{original_domain}"
else
email
end
end

def display_initials_or_uid
if display_name.present?
display_name.split.map { |name| name[0].upcase }.join
Expand Down
23 changes: 12 additions & 11 deletions app/views/containers/_assignment_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@
</div>
<% end %>

<div class="mb-3">
<%= f.label :uid_display, 'User' %>
<%= text_field_tag :uid_display, nil,
class: 'form-control',
data: {
uid_lookup_target: 'displayInput',
action: 'input->uid-lookup#lookup'
} %>
<%= f.hidden_field :uid, data: { uid_lookup_target: 'uidInput' } %>
<div data-uid-lookup-target="results" class="autocomplete-results"></div>
<div data-controller="uid-lookup" data-uid-lookup-url-value="<%= user_lookup_path %>">
<div class="mb-3">
<%= label_tag :uid_display, 'User' %>
<%= text_field_tag :uid_display, nil,
class: 'form-control',
data: {
uid_lookup_target: 'displayInput',
action: 'input->uid-lookup#lookup'
} %>
<%= hidden_field_tag 'assignment[uid]', nil, data: { uid_lookup_target: 'uidInput' } %>
<div data-uid-lookup-target="results" class="autocomplete-results"></div>
</div>
</div>

<div class="mb-3">
Expand All @@ -45,4 +47,3 @@
<%= f.submit 'Add User', class: "btn btn-primary my-2" %>
<%= link_to "Cancel", container_path(container), class: "text-danger link_to", data: { turbo: false, method: :get } %>
<% end %>

14 changes: 13 additions & 1 deletion app/views/contest_instances/_manage_judges.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
<div class="card mb-3 border-<%= round.active ? 'success' : (round.completed ? 'secondary' : 'warning') %>">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Round <%= round.round_number %></h5>
<% if round.judges.any? %>
<%= button_to send_instructions_container_contest_description_contest_instance_judging_round_path(
@container, @contest_description, contest_instance, round
),
method: :post,
class: "btn btn-sm btn-outline-success",
data: {
turbo_confirm: "Send judging instructions to #{pluralize(round.judges.count, 'judge')}?"
} do %>
<i class="bi bi-envelope me-1"></i>Send judging instructions
<% end %>
<% end %>
<span class="badge bg-<%= round.active ? 'success' : (round.completed ? 'secondary' : 'warning') %>">
<%= round.active ? 'Active' : (round.completed ? 'Completed' : 'Pending') %>
</span>
Expand All @@ -22,6 +34,7 @@
<div class="col">
<div class="d-flex align-items-center">
<strong class="me-3"><i class="bi bi-people me-1"></i> Judges Assigned:</strong>
</div>
<div class="d-flex flex-wrap">
<% if round.judges.any? %>
<% round.judges.each do |judge| %>
Expand All @@ -36,7 +49,6 @@
<em class="text-muted">No judges assigned to this round</em>
<% end %>
</div>
</div>
</div>
</div>

Expand Down
22 changes: 14 additions & 8 deletions app/views/judging_assignments/_judging_pool.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,20 @@
<div class="card-body">
<p>Start typing the name or email address of a judge to search for them system-wide. If you don't find the judge you're looking for, you can create a new judge using the "Create a new judge ..." form below.</p>
<%= form_with(model: [@container, @contest_description, @contest_instance, JudgingAssignment.new], local: true) do |f| %>
<div class="form-group">
<%= f.label :user_id, "Select Judge" %>
<%= f.select :user_id,
@available_judges.map { |u| ["#{u.first_name} #{u.last_name} (#{display_email(u.email)})", u.id] },
{ prompt: "Select a judge" },
class: "form-control" %>
<div data-controller="uid-lookup" data-uid-lookup-url-value="<%= judge_lookup_container_contest_description_contest_instance_judging_assignments_path(@container, @contest_description, @contest_instance) %>">
<div class="mb-3">
<%= label_tag :uid_display, 'Judge' %>
<%= text_field_tag :uid_display, nil,
class: 'form-control',
data: {
uid_lookup_target: 'displayInput',
action: 'input->uid-lookup#lookup'
} %>
<%= hidden_field_tag 'judging_assignment[user_id]', nil, data: { uid_lookup_target: 'uidInput' } %>
<div data-uid-lookup-target="results" class="autocomplete-results"></div>
</div>
<%= f.submit "Assign Judge", class: "btn btn-primary mt-3" %>
</div>
<%= f.submit "Assign Judge", class: "btn btn-primary mt-3" %>
<% end %>
</div>
</div>
Expand All @@ -57,7 +63,7 @@
</div>
<div id="createJudgeCollapse" class="accordion-collapse collapse" aria-labelledby="createJudgeHeader" data-bs-parent="#createJudgeAccordion">
<div class="accordion-body">
<p>Use this form to create a new judge and assign them to the pool of judges for this contest instance. The new judge will receive an email with instructions on how to log in to the judging dashboard.</p>
<p>Use this form to create a new judge and assign them to the pool of judges for this contest instance.</p>
<%= form_with(url: create_judge_container_contest_description_contest_instance_judging_assignments_path(@container, @contest_description, @contest_instance), local: true, html: { novalidate: true }) do |f| %>
<div class="form-group mb-3">
<%= f.label :email, "Email Address" %>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<div style="max-width: 600px; margin: 0 auto; padding: 20px; font-family: Arial, sans-serif;">
<h1 style="color: #003366; font-size: 24px; margin-bottom: 20px;">
Judging Instructions for <%= @contest_description.name %>
</h1>

<p style="font-size: 16px; line-height: 1.6; color: #333;">
Dear <%= @judge.display_name_or_first_name_last_name %>,
</p>

<p style="font-size: 16px; line-height: 1.6; color: #333;">
You have been assigned as a judge for <strong>Round <%= @judging_round.round_number %></strong> of the <%= @contest_description.name %> contest.
</p>

<div style="background-color: #f0f7ff; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2 style="color: #003366; font-size: 18px; margin-bottom: 10px;">Judging Period</h2>
<p style="font-size: 16px; line-height: 1.6; color: #333; margin: 5px 0;">
<strong>Start Date:</strong> <%= I18n.l(@judging_round.start_date, format: :long) %><br>
<strong>End Date:</strong> <%= I18n.l(@judging_round.end_date, format: :long) %>
</p>
</div>

<% if @judging_round.required_entries_count > 0 %>
<div style="background-color: #fff5e6; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2 style="color: #003366; font-size: 18px; margin-bottom: 10px;">Requirements</h2>
<p style="font-size: 16px; line-height: 1.6; color: #333; margin: 5px 0;">
<strong>Minimum entries to evaluate:</strong> <%= @judging_round.required_entries_count %>
</p>
<% if @judging_round.require_internal_comments %>
<p style="font-size: 16px; line-height: 1.6; color: #333; margin: 5px 0;">
<strong>Internal comments:</strong> Required (minimum <%= @judging_round.min_internal_comment_words %> words)
</p>
<% end %>
<% if @judging_round.require_external_comments %>
<p style="font-size: 16px; line-height: 1.6; color: #333; margin: 5px 0;">
<strong>External comments:</strong> Required (minimum <%= @judging_round.min_external_comment_words %> words)<br>
<em style="font-size: 14px;">Note: External comments will be shared with applicants</em>
</p>
<% end %>
</div>
<% end %>

<% if @special_instructions.present? %>
<div style="background-color: #f9f9f9; padding: 20px; border-radius: 5px; margin: 20px 0; border: 1px solid #ddd;">
<h2 style="color: #003366; font-size: 18px; margin-bottom: 15px;">Special Instructions</h2>
<div style="font-size: 16px; line-height: 1.6; color: #333;">
<%= simple_format(@special_instructions) %>
</div>
</div>
<% end %>

<div style="background-color: #e6f3ff; padding: 15px; border-radius: 5px; margin: 20px 0; border: 1px solid #b3d9ff;">
<h3 style="color: #003366; font-size: 16px; margin-bottom: 10px;">Important: Email Account Access</h3>
<p style="font-size: 14px; line-height: 1.6; color: #333; margin: 5px 0;">
Only the email account <strong><%= @judge_email %></strong> has access to the judging system.
</p>
<p style="font-size: 14px; line-height: 1.6; color: #333; margin: 5px 0;">
If you need to use a different email account, please contact us to adjust your access.
</p>
<% unless @judge_email.end_with?('@umich.edu') %>
<p style="font-size: 14px; line-height: 1.6; color: #333; margin: 10px 0 5px 0;">
<strong>For non-@umich.edu accounts:</strong> You'll need to set up a Friend account at
<a href="https://friend.weblogin.umich.edu/friend/" style="color: #0066cc;">friend.weblogin.umich.edu</a>
</p>
<% end %>
</div>

<div style="text-align: center; margin: 30px 0;">
<%= link_to "Go to Judging Dashboard", judge_dashboard_index_url,
style: "display: inline-block; padding: 12px 30px; background-color: #00274C; color: white; text-decoration: none; border-radius: 5px; font-size: 16px;" %>
</div>

<p style="font-size: 16px; line-height: 1.6; color: #333;">
If you have any questions about the judging process, please contact us at
<a href="mailto:<%= @contact_email %>" style="color: #0066cc;"><%= @contact_email %></a>.
</p>

<p style="font-size: 16px; line-height: 1.6; color: #333;">
Thank you for your time and commitment to this contest.
</p>

<hr style="border: none; border-top: 1px solid #ddd; margin: 30px 0;">

<p style="font-size: 14px; color: #666; text-align: center;">
This email was sent from LSA Evaluate.<br>
University of Michigan
</p>
</div>
Loading
Loading