Skip to content
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ GEM
uber (0.1.0)
unaccent (0.4.0)
unicode-display_width (2.5.0)
uri (0.13.2)
uri (0.13.3)
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
Expand Down
29 changes: 27 additions & 2 deletions app/controllers/judging_rounds_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,37 @@ def send_instructions
return
end

# Get selected judge assignment IDs, or use all if none selected
selected_assignment_ids = params[:judge_assignment_ids]&.compact_blank || []

if selected_assignment_ids.any?
assignments = @judging_round.round_judge_assignments.active.includes(:user).where(id: selected_assignment_ids)
else
# If no selection, send to all (backward compatibility)
assignments = @judging_round.round_judge_assignments.active.includes(:user)
end

if assignments.empty?
redirect_to container_contest_description_contest_instance_judging_assignments_path(
@container, @contest_description, @contest_instance
), alert: 'No judges selected.'
return
end

# Get collection administrator emails if copy requested
admin_emails = []
if params[:send_copy_to_admin] == '1'
admin_emails = @container.assignments.container_administrators.includes(:user).map { |a| a.user.normalize_email }
end

sent_count = 0
failed_emails = []

@judging_round.round_judge_assignments.active.includes(:user).each do |assignment|
assignments.each do |assignment|
begin
JudgingInstructionsMailer.send_instructions(assignment).deliver_later
mail = JudgingInstructionsMailer.send_instructions(assignment, cc_emails: admin_emails)
mail.deliver_later
assignment.update_column(:instructions_sent_at, Time.current)
sent_count += 1
rescue => e
failed_emails << assignment.user.email
Expand Down
5 changes: 4 additions & 1 deletion app/mailers/judging_instructions_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class JudgingInstructionsMailer < ApplicationMailer
def send_instructions(round_judge_assignment)
def send_instructions(round_judge_assignment, cc_emails: [])
@round_judge_assignment = round_judge_assignment
@judge = @round_judge_assignment.user
@judging_round = @round_judge_assignment.judging_round
Expand All @@ -23,6 +23,9 @@ def send_instructions(round_judge_assignment)
subject: subject
}

# Add CC to collection administrators if provided
mail_options[:cc] = cc_emails if cc_emails.any?

# 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?
Expand Down
13 changes: 7 additions & 6 deletions app/models/round_judge_assignment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
#
# Table name: round_judge_assignments
#
# id :bigint not null, primary key
# active :boolean default(TRUE)
# created_at :datetime not null
# updated_at :datetime not null
# judging_round_id :bigint not null
# user_id :bigint not null
# id :bigint not null, primary key
# active :boolean default(TRUE)
# instructions_sent_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# judging_round_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
Expand Down
115 changes: 102 additions & 13 deletions app/views/contest_instances/_manage_judges.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@
<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 %>
<button type="button"
class="btn btn-sm btn-outline-success"
data-bs-toggle="modal"
data-bs-target="#sendInstructionsModal<%= round.id %>">
<i class="bi bi-envelope me-1"></i>Send judging instructions
<% end %>
</button>
<% end %>
<span class="badge bg-<%= round.active ? 'success' : (round.completed ? 'secondary' : 'warning') %>">
<%= round.active ? 'Active' : (round.completed ? 'Completed' : 'Pending') %>
Expand All @@ -36,13 +32,18 @@
<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| %>
<span class="badge bg-secondary text-white me-2 mb-1"
<% assignments = round.round_judge_assignments.active.includes(:user) %>
<% if assignments.any? %>
<% assignments.each do |assignment| %>
<% judge = assignment.user %>
<span class="badge bg-<%= assignment.instructions_sent_at ? 'success' : 'secondary' %> text-white me-2 mb-1 d-inline-flex align-items-center"
data-bs-toggle="tooltip"
data-bs-html="true"
data-bs-title="<%= h(judge.display_name_or_first_name_last_name) %><br><%= h(display_email(judge.email)) %>">
data-bs-title="<%= h(judge.display_name_or_first_name_last_name) %><br><%= h(display_email(judge.email)) %><% if assignment.instructions_sent_at %><br><small>Instructions sent: <%= I18n.l(assignment.instructions_sent_at, format: :short) %></small><% end %>">
<%= judge.display_name_and_uid %>
<% if assignment.instructions_sent_at %>
<i class="bi bi-check-circle-fill ms-1" title="Instructions sent"></i>
<% end %>
</span>
<% end %>
<% else %>
Expand Down Expand Up @@ -91,6 +92,94 @@
<% end %>
</div>
</div>

<!-- Modal for selecting judges to send instructions -->
<div class="modal fade" id="sendInstructionsModal<%= round.id %>" tabindex="-1" aria-labelledby="sendInstructionsModalLabel<%= round.id %>" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<%= form_with url: send_instructions_container_contest_description_contest_instance_judging_round_path(
@container, @contest_description, contest_instance, round
),
method: :post,
data: {
controller: "checkboxselect",
action: "submit->checkboxselect#submitForm",
checkboxselect_error_text_value: "Please select at least one judge."
} do |f| %>
<div class="modal-header">
<h5 class="modal-title" id="sendInstructionsModalLabel<%= round.id %>">
Send Judging Instructions - Round <%= round.round_number %>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-3">Select which judges should receive the judging instructions email:</p>
<div class="form-check mb-3">
<%= check_box_tag "select_all_#{round.id}",
"1",
false,
class: "form-check-input",
data: {
checkboxselect_target: "checkboxAll",
action: "change->checkboxselect#toggleCheckbox"
} %>
<%= label_tag "select_all_#{round.id}", "Select All", class: "form-check-label" %>
</div>
<hr>
<div data-checkboxselect-target="errorMessage" class="text-danger mb-2"></div>
<div class="border rounded p-3" style="max-height: 400px; overflow-y: auto;">
<% assignments = round.round_judge_assignments.active.includes(:user) %>
<% assignments.each do |assignment| %>
<% judge = assignment.user %>
<div class="form-check mb-2">
<%= check_box_tag "judge_assignment_ids[]",
assignment.id,
false,
id: "judge_assignment_#{assignment.id}",
class: "form-check-input",
data: {
checkboxselect_target: "checkbox",
action: "change->checkboxselect#toggleCheckboxAll"
} %>
<div class="d-flex align-items-center">
<%= label_tag "judge_assignment_#{assignment.id}",
judge.display_name_and_uid,
class: "form-check-label me-2" %>
<% if assignment.instructions_sent_at %>
<span class="badge bg-success text-white ms-2">
<i class="bi bi-check-circle me-1"></i>Sent <%= I18n.l(assignment.instructions_sent_at, format: :short) %>
</span>
<% end %>
</div>
</div>
<% end %>
</div>
<hr class="my-3">
<div class="form-check">
<%= check_box_tag "send_copy_to_admin",
"1",
false,
id: "send_copy_to_admin_#{round.id}",
class: "form-check-input" %>
<%= label_tag "send_copy_to_admin_#{round.id}",
"Send a copy to Collection Administrator(s)",
class: "form-check-label" %>
<% admin_emails = @container.assignments.container_administrators.includes(:user).map { |a| a.user.email } %>
<% if admin_emails.any? %>
<small class="form-text text-muted d-block">
Copy will be sent to: <%= admin_emails.join(', ') %>
</small>
<% end %>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<%= f.submit "Send Instructions", class: "btn btn-success" %>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
</div>
26 changes: 26 additions & 0 deletions config/initializers/mailer_previews.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

# Enable mailer previews in production and staging with authorization
# This initializer allows mailer previews to be accessed by authorized users (Collection Administrators and Axis Mundi)

if Rails.env.production? || Rails.env.staging?
Rails.application.config.to_prepare do
# Override Rails::MailersController to add authentication and authorization
if defined?(Rails::MailersController)
Rails::MailersController.class_eval do
include Devise::Controllers::Helpers if defined?(Devise::Controllers::Helpers)

before_action :authenticate_user!
before_action :authorize_mailer_preview_access!

private

def authorize_mailer_preview_access!
unless current_user&.axis_mundi? || current_user&.administrator?
redirect_to root_path, alert: 'You are not authorized to access mailer previews.'
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddInstructionsSentAtToRoundJudgeAssignments < ActiveRecord::Migration[7.2]
def change
add_column :round_judge_assignments, :instructions_sent_at, :datetime
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"postcss-cli": "^11.0.0",
"sass": "^1.70.0",
"sortablejs": "^1.15.6",
"trix": "^2.1.15"
"trix": "^2.1.16"
},
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets",
Expand Down
Loading