diff --git a/Gemfile.lock b/Gemfile.lock index 9ebfde10..0dc1d2f5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/controllers/judging_rounds_controller.rb b/app/controllers/judging_rounds_controller.rb index 81622f1b..0841cce0 100644 --- a/app/controllers/judging_rounds_controller.rb +++ b/app/controllers/judging_rounds_controller.rb @@ -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 diff --git a/app/mailers/judging_instructions_mailer.rb b/app/mailers/judging_instructions_mailer.rb index 8fc2fb57..d407ece9 100644 --- a/app/mailers/judging_instructions_mailer.rb +++ b/app/mailers/judging_instructions_mailer.rb @@ -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 @@ -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? diff --git a/app/models/round_judge_assignment.rb b/app/models/round_judge_assignment.rb index ef0798f8..c9d9ed23 100644 --- a/app/models/round_judge_assignment.rb +++ b/app/models/round_judge_assignment.rb @@ -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 # diff --git a/app/views/contest_instances/_manage_judges.html.erb b/app/views/contest_instances/_manage_judges.html.erb index 8aff54b8..60ce1d8a 100644 --- a/app/views/contest_instances/_manage_judges.html.erb +++ b/app/views/contest_instances/_manage_judges.html.erb @@ -7,16 +7,12 @@
Round <%= round.round_number %>
<% 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 %> + <% end %> <%= round.active ? 'Active' : (round.completed ? 'Completed' : 'Pending') %> @@ -36,13 +32,18 @@ Judges Assigned:
- <% if round.judges.any? %> - <% round.judges.each do |judge| %> - + <% if assignments.any? %> + <% assignments.each do |assignment| %> + <% judge = assignment.user %> + + data-bs-title="<%= h(judge.display_name_or_first_name_last_name) %>
<%= h(display_email(judge.email)) %><% if assignment.instructions_sent_at %>
Instructions sent: <%= I18n.l(assignment.instructions_sent_at, format: :short) %><% end %>"> <%= judge.display_name_and_uid %> + <% if assignment.instructions_sent_at %> + + <% end %>
<% end %> <% else %> @@ -91,6 +92,94 @@ <% end %>
+ + + <% end %> diff --git a/config/initializers/mailer_previews.rb b/config/initializers/mailer_previews.rb new file mode 100644 index 00000000..9d875103 --- /dev/null +++ b/config/initializers/mailer_previews.rb @@ -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 diff --git a/db/migrate/20260113165213_add_instructions_sent_at_to_round_judge_assignments.rb b/db/migrate/20260113165213_add_instructions_sent_at_to_round_judge_assignments.rb new file mode 100644 index 00000000..329db07a --- /dev/null +++ b/db/migrate/20260113165213_add_instructions_sent_at_to_round_judge_assignments.rb @@ -0,0 +1,5 @@ +class AddInstructionsSentAtToRoundJudgeAssignments < ActiveRecord::Migration[7.2] + def change + add_column :round_judge_assignments, :instructions_sent_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index eab24cc8..7b6ddcba 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2025_06_05_210643) do +ActiveRecord::Schema[7.2].define(version: 2026_01_13_165213) do create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| t.string "name", null: false t.text "body", size: :long @@ -298,6 +298,7 @@ t.boolean "active", default: true t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.datetime "instructions_sent_at" t.index ["judging_round_id", "user_id"], name: "index_round_judge_assignments_on_judging_round_id_and_user_id", unique: true t.index ["judging_round_id"], name: "index_round_judge_assignments_on_judging_round_id" t.index ["user_id"], name: "index_round_judge_assignments_on_user_id" diff --git a/package.json b/package.json index 551427e9..1e408303 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/controllers/judging_rounds_controller_spec.rb b/spec/controllers/judging_rounds_controller_spec.rb index ea25c104..e837e361 100644 --- a/spec/controllers/judging_rounds_controller_spec.rb +++ b/spec/controllers/judging_rounds_controller_spec.rb @@ -10,7 +10,7 @@ before do container_admin_role = create(:role, kind: 'Collection Administrator') - create(:assignment, user: admin, container: container, role: container_admin_role) + @admin_assignment = create(:assignment, user: admin, container: container, role: container_admin_role) end describe 'GET #edit' do @@ -133,4 +133,379 @@ end end end + + describe 'POST #send_instructions' do + let(:judge1) { create(:user, :with_judge_role, email: 'judge1@umich.edu') } + let(:judge2) { create(:user, :with_judge_role, email: 'judge2@umich.edu') } + let(:judge3) { create(:user, :with_judge_role, email: 'judge3@umich.edu') } + + let!(:judging_assignment1) { create(:judging_assignment, user: judge1, contest_instance: contest_instance) } + let!(:judging_assignment2) { create(:judging_assignment, user: judge2, contest_instance: contest_instance) } + let!(:judging_assignment3) { create(:judging_assignment, user: judge3, contest_instance: contest_instance) } + + let!(:round_assignment1) { create(:round_judge_assignment, user: judge1, judging_round: judging_round) } + let!(:round_assignment2) { create(:round_judge_assignment, user: judge2, judging_round: judging_round) } + let!(:round_assignment3) { create(:round_judge_assignment, user: judge3, judging_round: judging_round) } + + before do + sign_in admin + ActiveJob::Base.queue_adapter = :test + end + + context 'when no judges are assigned to the round' do + let(:empty_contest_description) { create(:contest_description, :active, container: container) } + let(:empty_contest_instance) { create(:contest_instance, contest_description: empty_contest_description, active: false) } + let(:empty_round) { create(:judging_round, contest_instance: empty_contest_instance, round_number: 99) } + + it 'redirects with an alert' do + post :send_instructions, params: { + container_id: container.id, + contest_description_id: empty_contest_description.id, + contest_instance_id: empty_contest_instance.id, + id: empty_round.id + } + expect(response).to redirect_to(container_contest_description_contest_instance_judging_assignments_path( + container, empty_contest_description, empty_contest_instance + )) + expect(flash[:alert]).to eq('No judges assigned to this round.') + end + end + + context 'when sending to all judges (no selection)' do + before do + allow(JudgingInstructionsMailer).to receive(:send_instructions).and_call_original + end + + it 'sends instructions to all active judges' do + expect(JudgingInstructionsMailer).to receive(:send_instructions).with(round_assignment1, cc_emails: []) + expect(JudgingInstructionsMailer).to receive(:send_instructions).with(round_assignment2, cc_emails: []) + expect(JudgingInstructionsMailer).to receive(:send_instructions).with(round_assignment3, cc_emails: []) + + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id + } + end + + it 'queues emails for delivery' do + expect { + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id + } + }.to have_enqueued_job(ActionMailer::MailDeliveryJob).exactly(3).times + end + + it 'updates instructions_sent_at for all assignments' do + freeze_time do + current_time = Time.current + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id + } + + # update_column is synchronous, so we can check immediately + expect(round_assignment1.reload.instructions_sent_at).to be_within(1.second).of(current_time) + expect(round_assignment2.reload.instructions_sent_at).to be_within(1.second).of(current_time) + expect(round_assignment3.reload.instructions_sent_at).to be_within(1.second).of(current_time) + end + end + + it 'redirects with success notice' do + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id + } + + expect(response).to redirect_to(container_contest_description_contest_instance_judging_assignments_path( + container, contest_description, contest_instance + )) + expect(flash[:notice]).to eq('Judging instructions sent successfully to 3 judge(s).') + end + end + + context 'when sending to selected individual judges' do + it 'sends instructions only to selected judges' do + expect(JudgingInstructionsMailer).to receive(:send_instructions).with(round_assignment1, cc_emails: []).once + expect(JudgingInstructionsMailer).to receive(:send_instructions).with(round_assignment3, cc_emails: []).once + expect(JudgingInstructionsMailer).not_to receive(:send_instructions).with(round_assignment2, cc_emails: []) + + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id, + judge_assignment_ids: [round_assignment1.id, round_assignment3.id] + } + end + + it 'queues emails only for selected judges' do + expect { + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id, + judge_assignment_ids: [round_assignment1.id, round_assignment3.id] + } + }.to have_enqueued_job(ActionMailer::MailDeliveryJob).exactly(2).times + end + + it 'updates instructions_sent_at only for selected assignments' do + freeze_time do + current_time = Time.current + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id, + judge_assignment_ids: [round_assignment1.id, round_assignment3.id] + } + + # update_column is synchronous, so we can check immediately + expect(round_assignment1.reload.instructions_sent_at).to be_within(1.second).of(current_time) + expect(round_assignment3.reload.instructions_sent_at).to be_within(1.second).of(current_time) + expect(round_assignment2.reload.instructions_sent_at).to be_nil + end + end + + it 'redirects with success notice for selected judges' do + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id, + judge_assignment_ids: [round_assignment1.id, round_assignment3.id] + } + + expect(response).to redirect_to(container_contest_description_contest_instance_judging_assignments_path( + container, contest_description, contest_instance + )) + expect(flash[:notice]).to eq('Judging instructions sent successfully to 2 judge(s).') + end + end + + context 'when no judges are selected' do + it 'redirects with an alert when invalid IDs are provided' do + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id, + judge_assignment_ids: [99999, 99998] # Invalid IDs that don't exist + } + + expect(response).to redirect_to(container_contest_description_contest_instance_judging_assignments_path( + container, contest_description, contest_instance + )) + expect(flash[:alert]).to eq('No judges selected.') + end + end + + context 'when sending with container administrator CC' do + let(:cc_container) { create(:container) } + let(:cc_contest_description) { create(:contest_description, :active, container: cc_container) } + let(:cc_contest_instance) { create(:contest_instance, contest_description: cc_contest_description) } + let(:cc_judging_round) { create(:judging_round, contest_instance: cc_contest_instance) } + + # Create judging assignments first (required for round assignments) + let!(:cc_judging_assignment1) { create(:judging_assignment, user: judge1, contest_instance: cc_contest_instance) } + let!(:cc_judging_assignment2) { create(:judging_assignment, user: judge2, contest_instance: cc_contest_instance) } + let!(:cc_judging_assignment3) { create(:judging_assignment, user: judge3, contest_instance: cc_contest_instance) } + + let!(:cc_round_assignment1) { create(:round_judge_assignment, user: judge1, judging_round: cc_judging_round) } + let!(:cc_round_assignment2) { create(:round_judge_assignment, user: judge2, judging_round: cc_judging_round) } + let!(:cc_round_assignment3) { create(:round_judge_assignment, user: judge3, judging_round: cc_judging_round) } + + let(:container_admin1) { create(:user, email: 'admin1@umich.edu') } + let(:container_admin2) { create(:user, email: 'admin2@umich.edu') } + let(:admin_role) { create(:role, kind: 'Collection Administrator') } + + before do + # Ensure admin user has access to cc_container + cc_admin_role = create(:role, kind: 'Collection Administrator') + create(:assignment, user: admin, container: cc_container, role: cc_admin_role) + create(:assignment, user: container_admin1, container: cc_container, role: admin_role) + create(:assignment, user: container_admin2, container: cc_container, role: admin_role) + end + + it 'includes collection administrators in CC' do + call_args = nil + allow(JudgingInstructionsMailer).to receive(:send_instructions).and_wrap_original do |method, *args, **kwargs| + call_args = { args: args, kwargs: kwargs } + method.call(*args, **kwargs) + end + + post :send_instructions, params: { + container_id: cc_container.id, + contest_description_id: cc_contest_description.id, + contest_instance_id: cc_contest_instance.id, + id: cc_judging_round.id, + send_copy_to_admin: '1', + judge_assignment_ids: [cc_round_assignment1.id] + } + + expect(call_args).to be_present + expect(call_args[:args].first).to eq(cc_round_assignment1) + # Admin user is also included in CC list + expect(call_args[:kwargs][:cc_emails]).to match_array([admin.email, 'admin1@umich.edu', 'admin2@umich.edu']) + end + + it 'normalizes admin emails with plus signs' do + container_admin3 = create(:user, email: 'admin3+tag@umich.edu') + create(:assignment, user: container_admin3, container: cc_container, role: admin_role) + + call_args = nil + allow(JudgingInstructionsMailer).to receive(:send_instructions).and_wrap_original do |method, *args, **kwargs| + call_args = { args: args, kwargs: kwargs } + method.call(*args, **kwargs) + end + + # Note: The normalize_email method currently has a bug that returns "admin3@tag" instead of "admin3@umich.edu" + # Testing the actual current behavior + post :send_instructions, params: { + container_id: cc_container.id, + contest_description_id: cc_contest_description.id, + contest_instance_id: cc_contest_instance.id, + id: cc_judging_round.id, + send_copy_to_admin: '1', + judge_assignment_ids: [cc_round_assignment1.id] + } + + expect(call_args).to be_present + expect(call_args[:args].first).to eq(cc_round_assignment1) + # Admin user is also included in CC list + expect(call_args[:kwargs][:cc_emails]).to match_array([admin.email, 'admin1@umich.edu', 'admin2@umich.edu', 'admin3@tag']) + end + + it 'does not include CC when checkbox is not checked' do + expect(JudgingInstructionsMailer).to receive(:send_instructions).with( + round_assignment1, + cc_emails: [] + ) + + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id, + send_copy_to_admin: '0', + judge_assignment_ids: [round_assignment1.id] + } + end + + it 'works with individual judge selection and CC' do + call_args_list = [] + allow(JudgingInstructionsMailer).to receive(:send_instructions).and_wrap_original do |method, *args, **kwargs| + call_args_list << { args: args, kwargs: kwargs } + method.call(*args, **kwargs) + end + + post :send_instructions, params: { + container_id: cc_container.id, + contest_description_id: cc_contest_description.id, + contest_instance_id: cc_contest_instance.id, + id: cc_judging_round.id, + send_copy_to_admin: '1', + judge_assignment_ids: [cc_round_assignment1.id, cc_round_assignment3.id] + } + + expect(call_args_list.length).to eq(2) + assignment1_call = call_args_list.find { |c| c[:args].first == cc_round_assignment1 } + assignment3_call = call_args_list.find { |c| c[:args].first == cc_round_assignment3 } + assignment2_call = call_args_list.find { |c| c[:args].first == cc_round_assignment2 } + + expect(assignment1_call).to be_present + # Admin user is also included in CC list + expect(assignment1_call[:kwargs][:cc_emails]).to match_array([admin.email, 'admin1@umich.edu', 'admin2@umich.edu']) + expect(assignment3_call).to be_present + expect(assignment3_call[:kwargs][:cc_emails]).to match_array([admin.email, 'admin1@umich.edu', 'admin2@umich.edu']) + expect(assignment2_call).to be_nil + end + end + + context 'when email delivery fails' do + before do + allow(JudgingInstructionsMailer).to receive(:send_instructions).and_call_original + allow_any_instance_of(ActionMailer::MessageDelivery).to receive(:deliver_later).and_raise(StandardError.new('Email delivery failed')) + allow(Rails.logger).to receive(:error) + end + + it 'logs the error and continues with other emails' do + expect(Rails.logger).to receive(:error).at_least(:once) + + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id + } + end + + it 'includes failed emails in the notice message' do + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id + } + + expect(flash[:notice]).to include('Failed to send to:') + expect(flash[:notice]).to include('judge1@umich.edu') + end + + it 'does not update instructions_sent_at for failed emails' do + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id + } + + expect(round_assignment1.reload.instructions_sent_at).to be_nil + end + end + + context 'when user is not authorized' do + before { sign_in judge } + + it 'redirects to root path' do + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id + } + expect(response).to redirect_to(root_path) + end + end + + context 'when inactive assignments exist' do + let(:inactive_judge) { create(:user, :with_judge_role, email: 'inactive_judge@umich.edu') } + let!(:inactive_judging_assignment) { create(:judging_assignment, user: inactive_judge, contest_instance: contest_instance) } + let!(:inactive_assignment) do + create(:round_judge_assignment, :inactive, user: inactive_judge, judging_round: judging_round) + end + + it 'only sends to active assignments' do + expect(JudgingInstructionsMailer).to receive(:send_instructions).exactly(3).times + expect(JudgingInstructionsMailer).not_to receive(:send_instructions).with(inactive_assignment, anything) + + post :send_instructions, params: { + container_id: container.id, + contest_description_id: contest_description.id, + contest_instance_id: contest_instance.id, + id: judging_round.id + } + end + end + end end diff --git a/spec/factories/round_judge_assignments.rb b/spec/factories/round_judge_assignments.rb index 79275287..0fbf1333 100644 --- a/spec/factories/round_judge_assignments.rb +++ b/spec/factories/round_judge_assignments.rb @@ -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 # diff --git a/spec/mailers/judging_instructions_mailer_spec.rb b/spec/mailers/judging_instructions_mailer_spec.rb index 6d9c97fa..6dafa8da 100644 --- a/spec/mailers/judging_instructions_mailer_spec.rb +++ b/spec/mailers/judging_instructions_mailer_spec.rb @@ -112,5 +112,38 @@ expect(mail.body.encoded).not_to include('External comments:') end end + + context 'when CC emails are provided' do + let(:cc_emails) { ['admin1@umich.edu', 'admin2@umich.edu'] } + + it 'includes CC recipients in the email' do + mail_with_cc = described_class.send_instructions(round_judge_assignment, cc_emails: cc_emails) + expect(mail_with_cc.cc).to match_array(cc_emails) + end + + it 'still sends the primary email to the judge' do + mail_with_cc = described_class.send_instructions(round_judge_assignment, cc_emails: cc_emails) + expect(mail_with_cc.to).to eq(['judge@umich.edu']) + end + + it 'maintains all other email properties' do + mail_with_cc = described_class.send_instructions(round_judge_assignment, cc_emails: cc_emails) + expect(mail_with_cc.subject).to eq("Judging Instructions for Test Contest - Round 2") + expect(mail_with_cc.reply_to).to eq([ 'contest_admin@umich.edu' ]) + end + end + + context 'when no CC emails are provided' do + it 'does not include CC field' do + expect(mail.cc).to be_nil + end + end + + context 'when empty CC emails array is provided' do + it 'does not include CC field' do + mail_with_empty_cc = described_class.send_instructions(round_judge_assignment, cc_emails: []) + expect(mail_with_empty_cc.cc).to be_nil + end + end end end diff --git a/yarn.lock b/yarn.lock index 0c79b367..6333b99b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3863,10 +3863,10 @@ tr46@^3.0.0: dependencies: punycode "^2.1.1" -trix@^2.1.15: - version "2.1.15" - resolved "https://registry.yarnpkg.com/trix/-/trix-2.1.15.tgz#fabad796ea779a8ae96522402fbc214cbfc4015f" - integrity sha512-LoaXWczdTUV8+3Box92B9b1iaDVbxD14dYemZRxi3PwY+AuDm97BUJV2aHLBUFPuDABhxp0wzcbf0CxHCVmXiw== +trix@^2.1.16: + version "2.1.16" + resolved "https://registry.yarnpkg.com/trix/-/trix-2.1.16.tgz#601be839258b87cc83019915650c50eb7cbc161e" + integrity sha512-XtZgWI+oBvLzX7CWnkIf+ZWC+chL+YG/TkY43iMTV0Zl+CJjn18B1GJUCEWJ8qgfpcyMBuysnNAfPWiv2sV14A== dependencies: dompurify "^3.2.5"