From ec4591d5f740a008c572c3ced2be2a39ed8430fc Mon Sep 17 00:00:00 2001 From: Zach Wolfenbarger Date: Mon, 1 Jul 2024 18:05:38 -0500 Subject: [PATCH] Batch Aggregation mailer (#4352) * Add aggregation blob storage base URL to templates * Call mailer worker if status is updated * Mailer and mailer worker (and specs) * Hound * Prefer deliver_now --- .../api/v1/aggregations_controller.rb | 6 +++ app/mailers/aggregation_completed_mailer.rb | 19 +++++++ .../aggregation_complete.text.erb | 34 ++++++++++++ .../aggregation_completed_mailer_worker.rb | 12 +++++ kubernetes/deployment-production.tmpl | 1 + kubernetes/deployment-staging.tmpl | 1 + .../api/v1/aggregations_controller_spec.rb | 20 +++++++ .../aggregation_completed_mailer_spec.rb | 52 +++++++++++++++++++ ...ggregation_completed_mailer_worker_spec.rb | 18 +++++++ 9 files changed, 163 insertions(+) create mode 100644 app/mailers/aggregation_completed_mailer.rb create mode 100644 app/views/aggregation_completed_mailer/aggregation_complete.text.erb create mode 100644 app/workers/aggregation_completed_mailer_worker.rb create mode 100644 spec/mailers/aggregation_completed_mailer_spec.rb create mode 100644 spec/workers/aggregation_completed_mailer_worker_spec.rb diff --git a/app/controllers/api/v1/aggregations_controller.rb b/app/controllers/api/v1/aggregations_controller.rb index c107c7189..c8209a316 100644 --- a/app/controllers/api/v1/aggregations_controller.rb +++ b/app/controllers/api/v1/aggregations_controller.rb @@ -22,4 +22,10 @@ def create rescue AggregationClient::ConnectionError json_api_render(:service_unavailable, 'The aggregation service is unavailable or not responding') end + + def update + super do |agg| + AggregationCompletedMailerWorker.perform_async(agg.id) if update_params[:status] + end + end end diff --git a/app/mailers/aggregation_completed_mailer.rb b/app/mailers/aggregation_completed_mailer.rb new file mode 100644 index 000000000..4acc48e71 --- /dev/null +++ b/app/mailers/aggregation_completed_mailer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AggregationCompletedMailer < ApplicationMailer + layout false + + def aggregation_complete(agg) + @user = User.find(agg.user_id) + @email_to = @user.email + base_url = ENV.fetch('AGGREGATION_STORAGE_BASE_URL', '') + @zip_url = "#{base_url}/#{agg.uuid}/#{agg.uuid}.zip" + @reductions_url = "#{base_url}/#{agg.uuid}/reductions.csv" + + @success = agg.completed? + agg_status = @success ? 'was successful!' : 'failed' + subject = "Your workflow aggregation #{agg_status}" + + mail(to: @email_to, subject: subject) + end +end diff --git a/app/views/aggregation_completed_mailer/aggregation_complete.text.erb b/app/views/aggregation_completed_mailer/aggregation_complete.text.erb new file mode 100644 index 000000000..3e712a13e --- /dev/null +++ b/app/views/aggregation_completed_mailer/aggregation_complete.text.erb @@ -0,0 +1,34 @@ +Hello, + +Your workflow aggregation has finished processing. + +<% if @success %> +The workflow was aggregated successfully. + +Click here to download a zip file containing the following: + * Workflow classifications export + * Workflow export + * Extracts (one file per task) + * Reductions + +<%= @zip_url %> + +Click here to download the reductions file by itself: + +<%= @reductions_url %> + +<% else %> +The workflow failed to aggregate. Please contact us or try again. +<% end %> + +If you have questions or if there were any issues with your aggregation, please email contact@zooniverse.org. + +Cheers, +The Zooniverse Team + +This is an automated email, please do not respond. + +To manage your Zooniverse email subscription preferences visit https://zooniverse.org/settings + +To unsubscribe to all Zooniverse messages please visit https://zooniverse.org/unsubscribe +Please be aware that the above link will unsubscribe you from ALL Zooniverse emails. diff --git a/app/workers/aggregation_completed_mailer_worker.rb b/app/workers/aggregation_completed_mailer_worker.rb new file mode 100644 index 000000000..2019df20d --- /dev/null +++ b/app/workers/aggregation_completed_mailer_worker.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AggregationCompletedMailerWorker + include Sidekiq::Worker + + sidekiq_options queue: :data_high + + def perform(agg_id) + aggregation = Aggregation.find(agg_id) + AggregationCompletedMailer.aggregation_complete(aggregation).deliver_now + end +end diff --git a/kubernetes/deployment-production.tmpl b/kubernetes/deployment-production.tmpl index 5c39f42e0..49037fd15 100644 --- a/kubernetes/deployment-production.tmpl +++ b/kubernetes/deployment-production.tmpl @@ -121,6 +121,7 @@ data: TALK_API_APPLICATION: '1' USER_SUBJECT_LIMIT: '10000' AGGREGATION_HOST: http://aggregation-caesar/ + AGGREGATION_STORAGE_BASE_URL: https://aggregationdata.blob.core.windows.net --- apiVersion: apps/v1 kind: Deployment diff --git a/kubernetes/deployment-staging.tmpl b/kubernetes/deployment-staging.tmpl index 4cf3ebd90..0d4ea68b2 100644 --- a/kubernetes/deployment-staging.tmpl +++ b/kubernetes/deployment-staging.tmpl @@ -110,6 +110,7 @@ data: TALK_API_APPLICATION: '1' USER_SUBJECT_LIMIT: '100' AGGREGATION_HOST: http://aggregation-staging-app/ + AGGREGATION_STORAGE_BASE_URL: https://aggregationdata.blob.core.windows.net --- apiVersion: apps/v1 kind: Deployment diff --git a/spec/controllers/api/v1/aggregations_controller_spec.rb b/spec/controllers/api/v1/aggregations_controller_spec.rb index ef00b712a..927fdb780 100644 --- a/spec/controllers/api/v1/aggregations_controller_spec.rb +++ b/spec/controllers/api/v1/aggregations_controller_spec.rb @@ -107,6 +107,26 @@ end it_behaves_like 'is updatable' + + context 'with the mailer worker' do + before do + default_request scopes: scopes, user_id: authorized_user.id + allow(AggregationCompletedMailerWorker).to receive(:perform_async).with(resource.id) + end + + let(:params) { update_params.merge(id: resource.id) } + + it 'calls the mailer worker' do + put :update, params: params + expect(AggregationCompletedMailerWorker).to have_received(:perform_async).with(resource.id) + end + + it 'does not call the mailer if status is not an updated param' do + params[:aggregations].delete(:status) + put :update, params: params + expect(AggregationCompletedMailerWorker).not_to have_received(:perform_async) + end + end end describe '#destroy' do diff --git a/spec/mailers/aggregation_completed_mailer_spec.rb b/spec/mailers/aggregation_completed_mailer_spec.rb new file mode 100644 index 000000000..ce20634b5 --- /dev/null +++ b/spec/mailers/aggregation_completed_mailer_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AggregationCompletedMailer, type: :mailer do + let(:base_url) { 'https://example.com' } + + before do + allow(ENV).to receive(:fetch).with('AGGREGATION_STORAGE_BASE_URL', '').and_return(base_url) + end + + describe '#aggregation_complete' do + let(:mail) { described_class.aggregation_complete(aggregation) } + + context 'with a successful aggregation' do + let(:aggregation) { create(:aggregation, status: 'completed', uuid: 'asdf123asdf') } + let(:uuid) { aggregation.uuid } + + it 'mails the user' do + expect(mail.to).to match_array([aggregation.user.email]) + end + + it 'includes the subject' do + expect(mail.subject).to include('Your workflow aggregation was successful!') + end + + it 'includes the zip file link' do + expect(mail.body.encoded).to include("#{base_url}/#{uuid}/#{uuid}.zip") + end + + it 'includes the reductions file link' do + expect(mail.body.encoded).to include("#{base_url}/#{uuid}/reductions.csv") + end + + it 'comes from no-reply@zooniverse.org' do + expect(mail.from).to include('no-reply@zooniverse.org') + end + + it 'has the success statement' do + expect(mail.body.encoded).to include('The workflow was aggregated successfully.') + end + end + + context 'with a failed aggregation' do + let(:aggregation) { create(:aggregation, status: 'failed') } + + it 'reports the failures statement' do + expect(mail.body.encoded).to include('The workflow failed to aggregate.') + end + end + end +end diff --git a/spec/workers/aggregation_completed_mailer_worker_spec.rb b/spec/workers/aggregation_completed_mailer_worker_spec.rb new file mode 100644 index 000000000..4cce4c9fd --- /dev/null +++ b/spec/workers/aggregation_completed_mailer_worker_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AggregationCompletedMailerWorker do + let(:aggregation) { create(:aggregation) } + + it 'delivers the mail' do + expect { described_class.new.perform(aggregation.id) }.to change { ActionMailer::Base.deliveries.count }.by(1) + end + + context 'with missing attributes' do + it 'is missing an aggregation and does not send' do + expect { described_class.new.perform(nil) }.to not_change { ActionMailer::Base.deliveries.count } + .and raise_error(ActiveRecord::RecordNotFound) + end + end +end