Skip to content

Commit 8a7f5d7

Browse files
committed
Merge pull request #370 from alphagov/add-petition-emails
Add petition emails
2 parents 18bef54 + f716fce commit 8a7f5d7

53 files changed

Lines changed: 1062 additions & 244 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/assets/stylesheets/petitions/admin/views/_shared.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
margin-bottom: $gutter-half;
2121
}
2222

23+
.character-count {
24+
margin-bottom: 0;
25+
}
26+
2327
// Pagination
2428
.pagination {
2529
margin: $gutter-half 0;

app/controllers/admin/debate_outcomes_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def show
99

1010
def update
1111
if @debate_outcome.update(debate_outcome_params)
12-
EmailDebateOutcomesJob.run_later_tonight(@petition)
12+
EmailDebateOutcomesJob.run_later_tonight(petition: @petition)
1313
redirect_to [:admin, @petition], notice: 'Email will be sent overnight'
1414
else
1515
render 'admin/petitions/show'

app/controllers/admin/government_response_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def show
99

1010
def update
1111
if @government_response.update_attributes(government_response_params)
12-
EmailThresholdResponseJob.run_later_tonight(@petition)
12+
EmailThresholdResponseJob.run_later_tonight(petition: @petition)
1313
redirect_to [:admin, @petition], notice: 'Email will be sent overnight'
1414
else
1515
render 'admin/petitions/show'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
class Admin::PetitionEmailsController < Admin::AdminController
2+
respond_to :html
3+
before_action :fetch_petition
4+
before_action :build_email
5+
6+
def new
7+
render 'admin/petitions/show'
8+
end
9+
10+
def create
11+
if @email.update(email_params)
12+
EmailPetitionersJob.run_later_tonight(petition: @petition, email: @email)
13+
PetitionMailer.email_signer(@petition, feedback_signature, @email).deliver_now
14+
15+
redirect_to [:admin, @petition], notice: 'Email will be sent overnight'
16+
else
17+
render 'admin/petitions/show'
18+
end
19+
end
20+
21+
private
22+
23+
def fetch_petition
24+
@petition = Petition.moderated.find(params[:petition_id])
25+
end
26+
27+
def build_email
28+
@email = @petition.emails.build(sent_by: current_user.pretty_name)
29+
end
30+
31+
def email_params
32+
params.require(:petition_email).permit(:subject, :body)
33+
end
34+
35+
def feedback_signature
36+
FeedbackSignature.new(@petition)
37+
end
38+
end

app/controllers/admin/petitions_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def edit_scheduled_debate_date
2020
def update_scheduled_debate_date
2121
fetch_petition_for_scheduled_debate_date
2222
if @petition.update(update_scheduled_debate_date_params)
23-
EmailDebateScheduledJob.run_later_tonight(@petition)
23+
EmailDebateScheduledJob.run_later_tonight(petition: @petition)
2424
redirect_to admin_petition_url(@petition), notice: "Email will be sent overnight"
2525
else
2626
render :edit_scheduled_debate_date

app/controllers/admin/schedule_debate_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def show
88

99
def update
1010
if @petition.update_attributes(params_for_update)
11-
EmailDebateScheduledJob.run_later_tonight(@petition)
11+
EmailDebateScheduledJob.run_later_tonight(petition: @petition)
1212
redirect_to [:admin, @petition], notice: "Email will be sent overnight"
1313
else
1414
render 'admin/petitions/show'

app/jobs/concerns/email_all_petition_signatories.rb

Lines changed: 49 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,83 @@
11
module EmailAllPetitionSignatories
2-
extend ActiveSupport::Concern
2+
# Concern to add shared functionality to ActiveJob classes
3+
# that are responsible for enqueuing send email jobs
34

4-
#
5-
# Concern to add shared functionality to ActiveJob classes that are responsible
6-
# for enqueuing send email jobs
7-
#
5+
extend ActiveSupport::Concern
86

97
included do
10-
queue_as :default
8+
class_attribute :email_delivery_job_class
9+
class_attribute :timestamp_name
1110

12-
def self.run_later_tonight(petition)
13-
requested_at = Time.current
11+
attr_reader :petition, :requested_at
1412

15-
petition.set_email_requested_at_for(new.timestamp_name, to: requested_at)
13+
queue_as :default
14+
end
15+
16+
module ClassMethods
17+
def run_later_tonight(**args)
18+
petition, @requested_at = args[:petition], nil
1619

17-
set(wait_until: later_tonight).
18-
perform_later(petition, requested_at.getutc.iso8601(6))
20+
petition.set_email_requested_at_for(timestamp_name, to: requested_at)
21+
set(wait_until: later_tonight).perform_later(**args.merge(requested_at: requested_at_iso8601))
1922
end
2023

21-
def self.later_tonight
22-
1.day.from_now.at_midnight + rand(240).minutes + rand(60).seconds
24+
private
25+
26+
def requested_at
27+
@requested_at ||= Time.current
2328
end
24-
private_class_method :later_tonight
2529

26-
end
30+
def requested_at_iso8601
31+
requested_at.getutc.iso8601(6)
32+
end
2733

34+
def later_tonight
35+
midnight + random_interval
36+
end
2837

38+
def midnight
39+
requested_at.end_of_day
40+
end
2941

30-
def perform(petition, requested_at_string)
31-
@petition = petition
32-
@requested_at = requested_at_string.in_time_zone
33-
do_work!
42+
def random_interval
43+
rand(240).minutes + rand(60).seconds
44+
end
3445
end
3546

36-
def timestamp_name
37-
raise NotImplementedError.new "Including classes must implement #timestamp_name method"
47+
def perform(**args)
48+
@petition, @requested_at = args[:petition], args[:requested_at]
49+
50+
# If the petition has been updated since the job
51+
# was queued then don't send the emails.
52+
unless petition_has_been_updated?
53+
enqueue_send_email_jobs
54+
end
3855
end
3956

4057
private
4158

42-
attr_reader :petition, :requested_at
43-
44-
def do_work!
45-
return if petition_has_been_updated?
46-
47-
logger.info("Starting #{self.class.name} for petition '#{petition.action}' with email requested at: #{petition_timestamp}")
48-
enqueue_send_email_jobs
49-
logger.info("Finished #{self.class.name} for petition '#{petition.action}'")
50-
51-
end
52-
53-
#
5459
# Batches the signataries to send emails to in groups of 1000
5560
# and enqueues a job to do the actual sending
56-
#
5761
def enqueue_send_email_jobs
5862
signatures_to_email.find_each do |signature|
59-
email_delivery_job_class.perform_later(
60-
signature: signature,
61-
timestamp_name: timestamp_name,
62-
petition: petition,
63-
requested_at_as_string: requested_at.getutc.iso8601(6)
64-
)
63+
email_delivery_job_class.perform_later(**mailer_arguments(signature))
6564
end
6665
end
6766

67+
def mailer_arguments(signature)
68+
{
69+
signature: signature,
70+
timestamp_name: timestamp_name,
71+
petition: petition,
72+
requested_at: requested_at
73+
}
74+
end
75+
6876
# admins can ask to send the email multiple times and each time they
6977
# ask we enqueues a new job to send out emails with a new timestamp
7078
# we want to execute only the latest job enqueued
7179
def petition_has_been_updated?
72-
(petition_timestamp - requested_at).abs > 1
80+
(petition_timestamp - requested_at.in_time_zone).abs > 1
7381
end
7482

7583
def petition_timestamp
@@ -79,11 +87,4 @@ def petition_timestamp
7987
def signatures_to_email
8088
petition.signatures_to_email_for(timestamp_name)
8189
end
82-
83-
# The job class that handles the actual email sending for this job type
84-
def email_delivery_job_class
85-
raise NotImplementedError.new "Including classes must implement #email_delivery_job_class method"
86-
end
8790
end
88-
89-

app/jobs/concerns/email_delivery.rb

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,44 @@
11
module EmailDelivery
2-
extend ActiveSupport::Concern
3-
4-
#
52
# Send a single email to a recipient informing them about a petition that they have signed
63
# Implemented as a custom job rather than using action mailers #deliver_later so we can do
74
# extra checking before sending the email
8-
#
5+
6+
extend ActiveSupport::Concern
7+
8+
PERMANENT_FAILURES = [
9+
Net::SMTPFatalError,
10+
Net::SMTPSyntaxError
11+
]
12+
13+
TEMPORARY_FAILURES = [
14+
Net::SMTPAuthenticationError,
15+
Net::OpenTimeout,
16+
Net::SMTPServerBusy,
17+
Errno::ECONNRESET,
18+
Errno::ECONNREFUSED,
19+
Errno::ETIMEDOUT,
20+
Timeout::Error
21+
]
922

1023
included do
24+
attr_reader :signature, :timestamp_name, :petition, :requested_at
1125
queue_as :default
12-
end
1326

14-
def perform(signature:, timestamp_name:, petition:,
15-
requested_at_as_string:, mailer: PetitionMailer.name, logger: nil)
27+
rescue_from *PERMANENT_FAILURES do |exception|
28+
log_exception(exception)
29+
end
30+
31+
rescue_from *TEMPORARY_FAILURES do |exception|
32+
log_exception(exception)
33+
retry_job
34+
end
35+
end
1636

17-
@mailer = mailer.constantize
18-
@signature = signature
19-
@petition = petition
20-
@requested_at = requested_at_as_string.in_time_zone
21-
@timestamp_name = timestamp_name
22-
@logger = logger
37+
def perform(**args)
38+
@signature = args[:signature]
39+
@petition = args[:petition]
40+
@requested_at = args[:requested_at].in_time_zone
41+
@timestamp_name = args[:timestamp_name]
2342

2443
if can_send_email?
2544
send_email
@@ -29,29 +48,24 @@ def perform(signature:, timestamp_name:, petition:,
2948

3049
private
3150

32-
attr_reader :mailer, :signature, :timestamp_name, :petition, :requested_at
51+
def log_exception(exception)
52+
logger.info(log_message(exception))
53+
end
54+
55+
def log_message(exception)
56+
"#{exception.class.name} while sending email for #{self.class.name} to: #{signature.email} for #{petition.action}"
57+
end
3358

3459
def can_send_email?
3560
petition_has_not_been_updated? && email_not_previously_sent?
3661
end
3762

3863
def send_email
3964
create_email.deliver_now
65+
end
4066

41-
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Net::SMTPError, Timeout::Error => e
42-
# log that the send failed
43-
logger.info("#{e.class.name} while sending email for #{self.class.name} to: #{signature.email} for #{signature.petition.action}")
44-
45-
#
46-
# TODO: check the error and if it is a n AWS SES rate error:
47-
# 454 Throttling failure: Maximum sending rate exceeded
48-
# 454 Throttling failure: Daily message quota exceeded
49-
#
50-
# Then reschedule the send for a day later rather than keep failing
51-
#
52-
53-
# reraise to rerun the job later via the job retry mechanism
54-
raise e
67+
def mailer
68+
PetitionMailer
5569
end
5670

5771
def create_email
@@ -69,13 +83,11 @@ def petition_timestamp
6983
# We do not want to send the email if the petition has been updated
7084
# As email sending is enqueued straight after a petition has been updated
7185
def petition_has_not_been_updated?
72-
(petition_timestamp - requested_at).abs < 1
86+
(petition_timestamp - requested_at.in_time_zone).abs < 1
7387
end
7488

75-
#
7689
# Have we already sent an email for this petition version?
7790
# If we have then the timestamp for the signature will match the timestamp for the petition
78-
#
7991
def email_not_previously_sent?
8092
# check that the signature is still in the list of signatures
8193
petition.signatures_to_email_for(timestamp_name).where(id: signature.id).exists?

app/jobs/deliver_debate_outcome_email_job.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,4 @@ class DeliverDebateOutcomeEmailJob < ActiveJob::Base
44
def create_email
55
mailer.notify_signer_of_debate_outcome signature.petition, signature
66
end
7-
87
end

app/jobs/deliver_debate_scheduled_email_job.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,4 @@ class DeliverDebateScheduledEmailJob < ActiveJob::Base
44
def create_email
55
mailer.notify_signer_of_debate_scheduled signature.petition, signature
66
end
7-
87
end

0 commit comments

Comments
 (0)