diff --git a/app/controllers/evaluations_controller.rb b/app/controllers/evaluations_controller.rb index 09232d53..efcd8e3b 100644 --- a/app/controllers/evaluations_controller.rb +++ b/app/controllers/evaluations_controller.rb @@ -10,21 +10,18 @@ def index where(evaluator_submission_assignments: { user_id: current_user.id, status: [:assigned, :recused] - }). - includes(:challenge, :evaluation_form). - distinct + }).includes(:challenge, :evaluation_form).distinct end def submissions @phase = Phase.joins(:challenge_phases_evaluators). - where(challenge_phases_evaluators: { user_id: current_user.id }). - find(params[:id]) + where(challenge_phases_evaluators: { user_id: current_user.id }).find(params[:id]) @challenge = @phase.challenge @assigned_submissions = @phase.evaluator_submission_assignments. - where(evaluator: current_user).where(status: %i[assigned recused]). - includes(:submission, :evaluation).ordered_by_status + where(evaluator: current_user).where(status: %i[assigned recused]).includes(:submission, :evaluation). + ordered_by_status @submissions_count = helpers.calculate_submissions_count(@assigned_submissions) end @@ -82,6 +79,8 @@ def recuse current_user.evaluator_submission_assignments.where(submission_id: params[:submission_id]).first if EvaluatorRecusalService.new(@evaluator_submission_assignment).call + send_recusal_notification + flash[:notice] = I18n.t("evaluations.recusal.success") redirect_to submissions_evaluation_path(@evaluator_submission_assignment.phase), status: :see_other else @@ -119,6 +118,10 @@ def build_evaluation end end + def send_recusal_notification + NotificationMailer.recusal(@evaluator_submission_assignment).deliver_now + end + # Auth Helpers def can_access_evaluation? @evaluator_submission_assignment && @evaluator_submission_assignment.user_id == current_user.id diff --git a/app/controllers/evaluator_submission_assignments_controller.rb b/app/controllers/evaluator_submission_assignments_controller.rb index 59c717ac..28f4529d 100644 --- a/app/controllers/evaluator_submission_assignments_controller.rb +++ b/app/controllers/evaluator_submission_assignments_controller.rb @@ -27,6 +27,8 @@ def create status: :assigned ) if @evaluator_submission_assignment.save + NotificationMailer.evaluation_assignment(@evaluator_submission_assignment).deliver_now + redirect_to submission_path(@submission), notice: I18n.t("evaluator_submission_assignments.assigned.success") else redirect_to submission_path(@submission), notice: I18n.t("evaluator_submission_assignments.assigned.failure") @@ -84,16 +86,15 @@ def update_assignment_status(new_status) @assignment.update(status: new_status) end + def send_evaluation_assignment_notification(new_status) + NotificationMailer.evaluation_assignment(@assignment).deliver_now if new_status == :assigned + end + def handle_successful_update(new_status) + send_evaluation_assignment_notification(new_status) + flash[:success] = t("evaluator_submission_assignments.#{new_status}.success") - if request&.referer&.include?("submissions") - redirect_to request.referer - else - respond_to do |format| - format.html { redirect_to_assignment_path } - format.json { render json: { success: true, message: flash[:success] } } - end - end + handle_update_response end def handle_failed_update(new_status) @@ -110,4 +111,13 @@ def redirect_to_assignment_path evaluator_id: params[:evaluator_id] ) end + + def handle_update_response + return redirect_to request.referer if request&.referer&.include?("submissions") + + respond_to do |format| + format.html { redirect_to_assignment_path } + format.json { render json: { success: true, message: flash[:success] } } + end + end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 6bcb26d4..a089bcdb 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -3,6 +3,6 @@ # The main app mailer for sending emails. # The app does not process inbound messages. class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" + default from: "support@challenge.gov" layout "mailer" end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb new file mode 100644 index 00000000..4acbacef --- /dev/null +++ b/app/mailers/notification_mailer.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# Mailer class for sending notification emails related to evaluation invitations, +# evaluation assignments, evaluator role change requests, and recusals +class NotificationMailer < ApplicationMailer + include PhasesHelper + include EvaluationFormsHelper + + def evaluation_invitation(invitation) + setup_challenge_attrs(invitation.phase.challenge, invitation.phase) + @user = invitation + + mail( + to: invitation.email, + subject: I18n.t('mailers.evaluation_invitation.subject', challenge_title: invitation.phase.challenge.title) + ) + end + + def role_request(user, challenge, phase) + setup_challenge_attrs(challenge, phase) + @user = user + + mail( + to: user.email, + subject: I18n.t('mailers.evaluation_invitation.subject', challenge_title: challenge.title) + ) + end + + def evaluation_assignment(evaluator_submission_assignment) + setup_assignment_attrs(evaluator_submission_assignment) + @due_date = @submission.phase.end_date.strftime("%m/%d/%Y") + + mail( + to: @evaluator.email, + subject: I18n.t('mailers.evaluation_assignment.subject', submission_id: @submission.id) + ) + end + + def recusal(evaluator_submission_assignment) + setup_assignment_attrs(evaluator_submission_assignment) + + mail( + to: @challenge_managers.map(&:email), + subject: I18n.t('mailers.recusal.subject', submission_id: @submission.id) + ) + end + + private + + def setup_challenge_attrs(challenge, phase) + @challenge_phase_title = challenge_phase_title(challenge, phase) + @challenge_managers = challenge.challenge_managers.includes(:user).map(&:user) + attach_logo + end + + def setup_assignment_attrs(assignment) + @evaluator = assignment.evaluator + @submission = assignment.submission + setup_challenge_attrs(@submission.challenge, @submission.phase) + @assignment = assignment + end + + def attach_logo + logo_path = Rails.public_path.join('platform-assets/images/challenge_gov_logo.png') + attachments.inline['challenge_gov_logo.png'] = File.read(logo_path) + end +end diff --git a/app/services/evaluator_invitation_service.rb b/app/services/evaluator_invitation_service.rb index 3a9503ba..78e29eab 100644 --- a/app/services/evaluator_invitation_service.rb +++ b/app/services/evaluator_invitation_service.rb @@ -12,8 +12,9 @@ def handle_invitation(email, invitation_params) existing_invitation ? resend_invitation(existing_invitation) : create_new_invitation(invitation_params) end - # TODO: Implement sending the invitation email here def resend_invitation(invitation) + NotificationMailer.evaluation_invitation(invitation).deliver_now + if invitation.update(last_invite_sent: Time.current) { success: true, @@ -39,6 +40,7 @@ def create_new_invitation(invitation_params) ) ) if invitation.save + NotificationMailer.evaluation_invitation(invitation).deliver_now { success: true, message: I18n.t( diff --git a/app/services/evaluator_management_service.rb b/app/services/evaluator_management_service.rb index 8bd235e9..b2edae7d 100644 --- a/app/services/evaluator_management_service.rb +++ b/app/services/evaluator_management_service.rb @@ -22,6 +22,7 @@ def remove_evaluator(evaluator_type, evaluator_id) def self.accept_evaluator_invitation(user) invitations = EvaluatorInvitation.where(email: user.email) invitations.each do |invite| + user.update(first_name: invite.first_name, last_name: invite.last_name) ChallengePhasesEvaluator.create(challenge: invite.challenge, phase: invite.phase, user:) invite.destroy end @@ -77,6 +78,8 @@ def invalid_role(user) end def handle_evaluator_role_requested(user) + NotificationMailer.role_request(user, @challenge, @phase).deliver_now + user.update!(status: 'evaluator_role_requested') ChallengePhasesEvaluator.find_or_create_by(challenge: @challenge, phase: @phase, user:) { diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb index 3aac9002..3a6d579b 100644 --- a/app/views/layouts/mailer.html.erb +++ b/app/views/layouts/mailer.html.erb @@ -3,11 +3,61 @@ - - <%= yield %> + +
+ <%= yield %> +
+
+ <% unless action_name == 'role_request' %> +
+ <%= link_to "Login to Challenge.gov", + "#{Rails.env.development? ? 'http' : 'https'}://#{Rails.configuration.action_mailer.default_url_options[:host]}", + class: "mailer-button", + target: "_blank", + rel: "noopener noreferrer" %> +
+ <% end %> +
diff --git a/app/views/notification_mailer/_challenge_managers_contact.html.erb b/app/views/notification_mailer/_challenge_managers_contact.html.erb new file mode 100644 index 00000000..d0fb554d --- /dev/null +++ b/app/views/notification_mailer/_challenge_managers_contact.html.erb @@ -0,0 +1,9 @@ +<% if @challenge_managers.length > 1 %> + If you have any questions you may reach out to any of the challenge managers: + <% @challenge_managers.each do |cm| %> + <%= "#{cm.first_name} #{cm.last_name}" %> at <%= "#{cm.email}" %>, + <% end %> +<% else %> + If you have any questions you may reach out to the challenge manager <%= "#{@challenge_managers.first.first_name} #{@challenge_managers.first.last_name}" %> at <%= "#{@challenge_managers.first.email}" %> +<% end %> +or to team@challenge.gov. diff --git a/app/views/notification_mailer/evaluation_assignment.html.erb b/app/views/notification_mailer/evaluation_assignment.html.erb new file mode 100644 index 00000000..c909e329 --- /dev/null +++ b/app/views/notification_mailer/evaluation_assignment.html.erb @@ -0,0 +1,11 @@ +

You have been assigned to evaluate a submission.

+ +A challenge manager assigned you to evaluate <%= link_to "submission #{@submission.id}", new_submission_evaluation_url(@submission), target: "_blank" %> for <%= @challenge_phase_title %>. Evaluations are due by <%= @due_date %>. + +

+ +Please login to Challenge.gov via Login.gov to review and evaluate submissions assigned to you. + +

+ +<%= render 'challenge_managers_contact' %> diff --git a/app/views/notification_mailer/evaluation_invitation.html.erb b/app/views/notification_mailer/evaluation_invitation.html.erb new file mode 100644 index 00000000..8b0fa163 --- /dev/null +++ b/app/views/notification_mailer/evaluation_invitation.html.erb @@ -0,0 +1,12 @@ +

You have been invited to <%= @challenge_phase_title %> Evaluation Panel.

+ +A challenge manager invited you to participate in an evaluation process for <%= @challenge_phase_title %>. +To participate in the submission evaluation process, please create a Challenge.gov account via Login.gov. + +

+ +After you complete the account creation step, we will send you a separate email letting you know once a challenge manager assigns submissions for you to evaluate. + +

+ +<%= render 'challenge_managers_contact' %> diff --git a/app/views/notification_mailer/recusal.html.erb b/app/views/notification_mailer/recusal.html.erb new file mode 100644 index 00000000..aba7a802 --- /dev/null +++ b/app/views/notification_mailer/recusal.html.erb @@ -0,0 +1,11 @@ +

An evaluator recused from evaluating a submission you assigned them to.

+ +An evaluator <%= "#{@evaluator.email}" %> recused themselves from evaluating <%= link_to "submission #{@submission.id}", submission_url(@submission), target: "_blank" %> for <%= @challenge_phase_title %>. + +

+ +Please login to Challenge.gov via Login.gov to unassign the recused evaluator and assign another evaluator. + +

+ +<%= render 'challenge_managers_contact' %> diff --git a/app/views/notification_mailer/role_request.html.erb b/app/views/notification_mailer/role_request.html.erb new file mode 100644 index 00000000..76f779eb --- /dev/null +++ b/app/views/notification_mailer/role_request.html.erb @@ -0,0 +1,12 @@ +

You have been invited to <%= @challenge_phase_title %> Evaluation Panel.

+ +A challenge manager invited you to participate in an evaluation process for <%= @challenge_phase_title %>. You currently have a Challenge.gov account as a <%= @user.role %>. +To participate in the submission evaluation process, please respond to this email to request your role to be changed to an evaluator. + +

+ +After your Challenge.gov account is updated, we will send you a separate email letting you know once a challenge manager assigns submissions for you to evaluate. + +

+ +<%= render 'challenge_managers_contact' %> diff --git a/config/environments/dev.rb b/config/environments/dev.rb index 59363bd6..f943dfa8 100644 --- a/config/environments/dev.rb +++ b/config/environments/dev.rb @@ -75,6 +75,18 @@ config.action_mailer.perform_caching = false + config.action_mailer.delivery_method = :smtp + config.action_mailer.perform_deliveries = true + config.action_mailer.raise_delivery_errors = true + + config.action_mailer.smtp_settings = { + domain: ENV.fetch('HOST'), + address: ENV.fetch('SMTP_SERVER'), + port: ENV.fetch('SMTP_PORT', 587).to_i + } + + config.action_mailer.default_url_options = { host: ENV.fetch('HOST', 'challenge-dev.app.cloud.gov') } + # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/development.rb b/config/environments/development.rb index 7cd5367f..12d00522 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -50,6 +50,14 @@ # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + # Store emails locally for development + config.action_mailer.delivery_method = :file + config.action_mailer.file_settings = { + location: Rails.root.join('tmp/mails') + } + config.action_mailer.perform_deliveries = true + config.action_mailer.default_url_options = { host: "localhost:#{ENV.fetch('PORT', 3000)}" } + config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. @@ -84,4 +92,4 @@ # Raise error when a before_action's only/except options reference missing actions config.action_controller.raise_on_missing_callback_actions = true -end \ No newline at end of file +end diff --git a/config/environments/production.rb b/config/environments/production.rb index 59363bd6..b1c058eb 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -75,6 +75,18 @@ config.action_mailer.perform_caching = false + config.action_mailer.delivery_method = :smtp + config.action_mailer.perform_deliveries = true + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.smtp_settings = { + domain: ENV.fetch('HOST'), + address: ENV.fetch('SMTP_SERVER'), + port: ENV.fetch('SMTP_PORT', 587).to_i + } + + config.action_mailer.default_url_options = { host: ENV.fetch('HOST', 'challenge.gov') } + # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 59363bd6..b1c058eb 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -75,6 +75,18 @@ config.action_mailer.perform_caching = false + config.action_mailer.delivery_method = :smtp + config.action_mailer.perform_deliveries = true + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.smtp_settings = { + domain: ENV.fetch('HOST'), + address: ENV.fetch('SMTP_SERVER'), + port: ENV.fetch('SMTP_PORT', 587).to_i + } + + config.action_mailer.default_url_options = { host: ENV.fetch('HOST', 'challenge.gov') } + # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false diff --git a/config/environments/test.rb b/config/environments/test.rb index 9ad64b82..afac0c5b 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -52,6 +52,7 @@ # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.action_mailer.default_url_options = { host: "test.host" } # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr diff --git a/config/locales/en.yml b/config/locales/en.yml index 2f75a5e5..8bdefa20 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -122,4 +122,12 @@ en: text: "A user recused from evaluations for one of the challenge submissions. Please review the list of submissions and unassign a recused evaluator." recused_evaluator_submission: heading: "Recused Evaluator" - text: "This submission has a recused evaluator. Please unassign a recused evaluator." + text: "This submission has a recused evaluator. Please unassign a recused evaluator." + mailers: + evaluation_invitation: + subject: "You have been invited to %{challenge_title} Evaluation Panel" + evaluation_assignment: + subject: "You have been assigned to evaluate submission %{submission_id}" + recusal: + subject: "An evaluator has recused from evaluating submission %{submission_id}" + diff --git a/public/platform-assets/images/challenge_gov_logo.png b/public/platform-assets/images/challenge_gov_logo.png new file mode 100644 index 00000000..48594b50 Binary files /dev/null and b/public/platform-assets/images/challenge_gov_logo.png differ diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb new file mode 100644 index 00000000..e4e08581 --- /dev/null +++ b/spec/mailers/notification_mailer_spec.rb @@ -0,0 +1,144 @@ +require 'rails_helper' + +RSpec.describe NotificationMailer, type: :mailer do + let(:challenge) { create(:challenge, title: "Test Challenge") } + let(:phase) { create(:phase, challenge: challenge) } + let(:challenge_manager) { create(:user, role: "challenge_manager") } + let!(:challenge_manager_assignment) { create(:challenge_manager, user: challenge_manager, challenge: challenge) } + + shared_examples "includes challenge manager contact info" do + it "includes challenge manager contact information" do + expect(mail.body.encoded).to include(challenge_manager.email) + expect(mail.body.encoded).to include(challenge_manager.first_name) + expect(mail.body.encoded).to include(challenge_manager.last_name) + end + end + + describe "#evaluation_invitation" do + let(:invitation) { create(:evaluator_invitation, phase: phase, challenge: challenge) } + let(:mail) { described_class.evaluation_invitation(invitation) } + + it "renders the headers" do + expect(mail.subject).to eq(I18n.t("mailers.evaluation_invitation.subject", challenge_title: challenge.title)) + expect(mail.to).to eq([invitation.email]) + expect(mail.from).to eq(["support@challenge.gov"]) + end + + it "renders the body" do + expect(mail.body.encoded).to match(/You have been invited to #{challenge.title}/) + expect(mail.body.encoded).to match(/create a Challenge.gov account via Login.gov/) + end + + include_examples "includes challenge manager contact info" + + context "when resending invitation" do + let(:invitation) { create(:evaluator_invitation, phase: phase, challenge: challenge) } + + it "sends the invitation email again" do + expect { + EvaluatorManagementService.new(challenge, phase).resend_invitation(invitation) + }.to change { ActionMailer::Base.deliveries.count }.by(1) + + mail = ActionMailer::Base.deliveries.last + expect(mail.subject).to eq(I18n.t("mailers.evaluation_invitation.subject", challenge_title: challenge.title)) + expect(mail.to).to eq([invitation.email]) + end + end + end + + describe "#role_request" do + let(:user) { create(:user, role: "solver") } + let(:mail) { described_class.role_request(user, challenge, phase) } + + it "renders the headers" do + expect(mail.subject).to eq(I18n.t("mailers.evaluation_invitation.subject", challenge_title: challenge.title)) + expect(mail.to).to eq([user.email]) + expect(mail.from).to eq(["support@challenge.gov"]) + end + + it "renders the body" do + expect(mail.body.encoded).to match(/You have been invited to #{challenge.title}/) + expect(mail.body.encoded).to match(/request your role to be changed to an evaluator/) + expect(mail.body.encoded).to match(/currently have a Challenge.gov account as a #{user.role}/) + end + + include_examples "includes challenge manager contact info" + end + + describe "#evaluation_assignment" do + let(:evaluator) { create(:user, role: "evaluator") } + let(:submission) { create(:submission, challenge: challenge, phase: phase) } + let(:assignment) { create(:evaluator_submission_assignment, evaluator: evaluator, submission: submission, status: :assigned) } + let(:mail) { described_class.evaluation_assignment(assignment) } + + it "renders the headers" do + expect(mail.subject).to eq(I18n.t("mailers.evaluation_assignment.subject", submission_id: submission.id)) + expect(mail.to).to eq([evaluator.email]) + expect(mail.from).to eq(["support@challenge.gov"]) + end + + it "renders the body" do + due_date = phase.end_date.strftime("%m/%d/%Y") + expect(mail.body.encoded).to match(/You have been assigned to evaluate/) + expect(mail.body.encoded).to match(/submission #{submission.id}/) + expect(mail.body.encoded).to match(/Evaluations are due by #{due_date}/) + end + + include_examples "includes challenge manager contact info" + + context "when reassigning an evaluator" do + let(:unassigned_assignment) { create(:evaluator_submission_assignment, evaluator: evaluator, submission: submission, status: :unassigned) } + + it "sends assignment email when status changes to assigned" do + expect { + unassigned_assignment.update!(status: :assigned) + NotificationMailer.evaluation_assignment(unassigned_assignment).deliver_now + }.to change { ActionMailer::Base.deliveries.count }.by(1) + + mail = ActionMailer::Base.deliveries.last + expect(mail.subject).to eq(I18n.t("mailers.evaluation_assignment.subject", submission_id: submission.id)) + expect(mail.to).to eq([evaluator.email]) + expect(mail.body.encoded).to match(/You have been assigned to evaluate/) + expect(mail.body.encoded).to match(/submission #{submission.id}/) + end + end + end + + describe "#recusal" do + let(:evaluator) { create(:user, role: "evaluator") } + let(:submission) { create(:submission, challenge: challenge, phase: phase) } + let(:assignment) { create(:evaluator_submission_assignment, evaluator: evaluator, submission: submission, status: :recused) } + let(:mail) { described_class.recusal(assignment) } + + before do + challenge.challenge_managers.destroy_all + create(:challenge_manager, user: challenge_manager, challenge: challenge) + end + + it "renders the headers" do + expect(mail.subject).to eq(I18n.t("mailers.recusal.subject", submission_id: submission.id)) + expect(mail.to).to eq([challenge_manager.email]) + expect(mail.from).to eq(["support@challenge.gov"]) + end + + it "renders the body" do + expect(mail.body.encoded).to match(/An evaluator recused from evaluating/) + expect(mail.body.encoded).to match(/submission #{submission.id}/) + expect(mail.body.encoded).to match(/#{evaluator.email}/) + end + + context "with multiple challenge managers" do + let(:second_manager) { create(:user, role: "challenge_manager") } + + before do + create(:challenge_manager, user: second_manager, challenge: challenge) + end + + it "sends to all challenge managers" do + expect(mail.to).to match_array([challenge_manager.email, second_manager.email]) + end + end + + include_examples "includes challenge manager contact info" + end +end diff --git a/spec/requests/evaluations_spec.rb b/spec/requests/evaluations_spec.rb index b62e41e6..77d94f6f 100644 --- a/spec/requests/evaluations_spec.rb +++ b/spec/requests/evaluations_spec.rb @@ -179,7 +179,7 @@ end context "with assigned submissions" do - let(:submission) { create(:submission, phase: phase) } + let(:submission) { create(:submission, phase: phase, challenge: challenge) } let!(:assignment) do create(:evaluator_submission_assignment, submission: submission, @@ -762,7 +762,7 @@ context "when the evaluation does not exist" do let(:challenge) { create(:challenge) } let(:phase) { create(:phase, challenge: challenge) } - let(:submission) { create(:submission, phase: phase) } + let(:submission) { create(:submission, phase: phase, challenge: challenge) } let!(:evaluator_submission_assignment) do create(:evaluator_submission_assignment, submission: submission, @@ -774,6 +774,10 @@ expect do patch recuse_submission_evaluations_path(submission) end.to change { evaluator_submission_assignment.reload.status }.from("assigned").to("recused") + .and change { ActionMailer::Base.deliveries.count }.by(1) + + mail = ActionMailer::Base.deliveries.last + expect(mail.subject).to eq(I18n.t("mailers.recusal.subject", submission_id: submission.id)) expect(flash[:notice]).to eq(I18n.t("evaluations.recusal.success")) expect(response).to redirect_to(submissions_evaluation_path(phase)) @@ -797,7 +801,8 @@ context "when logged in as an evaluator" do let(:challenge) { create(:challenge) } let(:phase) { create(:phase, challenge: challenge) } - let(:submission) { create(:submission, phase: phase) } + let(:submission) { create(:submission, phase: phase, challenge: challenge) } + let(:challenge_manager) { create(:user, role: "challenge_manager") } let(:evaluation_form) { create(:evaluation_form, phase: phase, challenge: challenge) } let(:evaluator_submission_assignment) do create(:evaluator_submission_assignment, @@ -814,11 +819,21 @@ completed_at: Time.current) end - it "destroys evaluation when recusing" do + before do + log_in_user(current_user) + create(:challenge_manager, user: challenge_manager, challenge: challenge) + end + + it "destroys evaluation and sends recusal notification email when recusing" do expect do patch recuse_submission_evaluations_path(submission) end.to change { Evaluation.count }.by(-1). - and change { evaluator_submission_assignment.reload.status }.to("recused") + and change { evaluator_submission_assignment.reload.status }.to("recused"). + and change { ActionMailer::Base.deliveries.count }.by(1) + + mail = ActionMailer::Base.deliveries.last + expect(mail.subject).to eq(I18n.t("mailers.recusal.subject", submission_id: submission.id)) + expect(mail.to).to match_array(challenge.challenge_managers.map(&:user).map(&:email)) expect(response).to redirect_to(submissions_evaluation_path(phase)) expect(flash[:notice]).to eq(I18n.t("evaluations.recusal.success")) diff --git a/spec/requests/evaluator_submission_assignments_spec.rb b/spec/requests/evaluator_submission_assignments_spec.rb index f3e6a11d..97a724bf 100644 --- a/spec/requests/evaluator_submission_assignments_spec.rb +++ b/spec/requests/evaluator_submission_assignments_spec.rb @@ -63,13 +63,60 @@ end end + describe "POST /phase/:phase_id/evaluator_submission_assignments" do + let(:new_evaluator) { create(:user, role: 'evaluator') } + let(:new_submission) { create(:submission, challenge: challenge, phase: phase) } + + before do + ChallengePhasesEvaluator.create!(challenge: challenge, phase: phase, user: new_evaluator) + end + + it "creates assignment and sends notification email" do + expect do + post phase_evaluator_submission_assignments_path(phase), params: { + evaluator_id: new_evaluator.id, + submission_id: new_submission.id + } + end.to change { EvaluatorSubmissionAssignment.count }.by(1) + .and change { ActionMailer::Base.deliveries.count }.by(1) + + mail = ActionMailer::Base.deliveries.last + expect(mail.subject).to eq(I18n.t("mailers.evaluation_assignment.subject", + submission_id: new_submission.id)) + expect(mail.to).to eq([new_evaluator.email]) + + expect(flash[:notice]).to eq(I18n.t("evaluator_submission_assignments.assigned.success")) + expect(response).to redirect_to(submission_path(new_submission)) + end + + it "handles failed assignment creation" do + allow_any_instance_of(EvaluatorSubmissionAssignment).to receive(:save).and_return(false) + + post phase_evaluator_submission_assignments_path(phase), params: { + evaluator_id: new_evaluator.id, + submission_id: new_submission.id + } + + expect(flash[:notice]).to eq(I18n.t("evaluator_submission_assignments.assigned.failure")) + expect(response).to redirect_to(submission_path(new_submission)) + end + end + describe 'PATCH #update' do context 'when reassigning' do - it 'reassigns the evaluator successfully and updates counts' do - patch phase_evaluator_submission_assignment_path(phase, unassigned_assignment), - params: { status: :assigned, evaluator_id: evaluator.id } + it 'reassigns the evaluator and sends assignment notification' do + expect do + patch phase_evaluator_submission_assignment_path(phase, unassigned_assignment), + params: { status: :assigned, evaluator_id: evaluator.id } + end.to change { unassigned_assignment.reload.status }.from("unassigned").to("assigned") + .and change { ActionMailer::Base.deliveries.count }.by(1) + + mail = ActionMailer::Base.deliveries.last + expect(mail.subject).to eq(I18n.t("mailers.evaluation_assignment.subject", + submission_id: unassigned_submission.id)) + expect(mail.to).to eq([evaluator.email]) + expect(mail.body.encoded).to match(/submission #{unassigned_submission.id}/) - expect(unassigned_assignment.reload.status).to eq('assigned') expect(flash[:success]).to eq(I18n.t('evaluator_submission_assignments.assigned.success')) expect(response).to redirect_to(phase_evaluator_submission_assignments_path(phase, evaluator_id: evaluator.id)) end diff --git a/spec/services/evaluator_management_service_spec.rb b/spec/services/evaluator_management_service_spec.rb index 2f603100..21870f05 100644 --- a/spec/services/evaluator_management_service_spec.rb +++ b/spec/services/evaluator_management_service_spec.rb @@ -70,18 +70,33 @@ } end - it 'creates a new invitation' do - result = service.process_evaluator_invitation(email, invitation_params) - expect(result[:success]).to be true - expect(result[:message]).to include('Invitation sent') - expect(EvaluatorInvitation.find_by(email: email)).to be_present + it 'creates a new invitation and sends notification email' do + expect do + result = service.process_evaluator_invitation(email, invitation_params) + expect(result[:success]).to be true + expect(result[:message]).to include('Invitation sent') + expect(EvaluatorInvitation.find_by(email: email)).to be_present + end.to change { EvaluatorInvitation.count }.by(1) + .and change { ActionMailer::Base.deliveries.count }.by(1) + + mail = ActionMailer::Base.deliveries.last + expect(mail.subject).to eq(I18n.t("mailers.evaluation_invitation.subject", + challenge_title: challenge.title)) + expect(mail.to).to eq([email]) end - it 'resends an existing invitation' do + it 'resends an existing invitation with notification email' do create(:evaluator_invitation, challenge: challenge, phase: phase, email: email) result = service.process_evaluator_invitation(email, { email: email }) expect(result[:success]).to be true expect(result[:message]).to include('Invitation has been resent') + expect(EvaluatorInvitation.count).to eq(1) + + expect(ActionMailer::Base.deliveries.count).to eq(1) + mail = ActionMailer::Base.deliveries.last + expect(mail.subject).to eq(I18n.t("mailers.evaluation_invitation.subject", + challenge_title: challenge.title)) + expect(mail.to).to eq([email]) end end @@ -97,6 +112,11 @@ expect(result[:success]).to be true expect(result[:message]).to include('requires a role change to evaluator') expect(solver.reload.status).to eq('evaluator_role_requested') + + expect(ActionMailer::Base.deliveries.count).to eq(1) + mail = ActionMailer::Base.deliveries.last + expect(mail.subject).to eq(I18n.t("mailers.evaluation_invitation.subject", + challenge_title: challenge.title)) end it 'does not set evaluator_role_requested for users with evaluator role' do diff --git a/spec/system/evaluator_submission_assignment_spec.rb b/spec/system/evaluator_submission_assignment_spec.rb index 8170d7f8..6b07cdd9 100644 --- a/spec/system/evaluator_submission_assignment_spec.rb +++ b/spec/system/evaluator_submission_assignment_spec.rb @@ -5,7 +5,7 @@ let(:challenge) { create(:challenge) } let(:phase) { create(:phase, challenge: challenge) } let(:evaluator) { create(:user, role: 'evaluator') } - let(:submission) { create(:submission, phase: phase) } + let(:submission) { create(:submission, phase: phase, challenge: challenge) } let!(:evaluation_form) do create(:evaluation_form, phase: phase, challenge: challenge, closing_date: 1.month.from_now) end