Skip to content

Commit bdc020f

Browse files
committed
Implement Newsletter bulk delivery
1 parent d2abd65 commit bdc020f

File tree

16 files changed

+303
-8
lines changed

16 files changed

+303
-8
lines changed

app/controllers/admin/newsletters_controller.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
class Admin::NewslettersController < ApplicationController
2-
before_action :set_newsletter, only: %i[show edit update destroy]
2+
before_action :set_newsletter, only: %i[show edit update destroy deliver]
33

44
# GET /admin/newsletters
55
def index
@@ -45,6 +45,21 @@ def destroy
4545
redirect_to admin_newsletter_url, notice: "Newsletter was successfully destroyed.", status: :see_other
4646
end
4747

48+
# PATCH /admin/newsletters/1/deliver
49+
def deliver
50+
@newsletter = Newsletter.find(params[:id])
51+
recipients = deliver_live? ? User.subscribers : User.where(email: ApplicationMailer.test_recipients)
52+
label = deliver_live? ? "LIVE" : "TEST"
53+
54+
if recipients.empty?
55+
return redirect_to [:admin, @newsletter], alert: "No recipients found."
56+
end
57+
58+
NewsletterNotifier.deliver_to(recipients, newsletter: @newsletter)
59+
60+
redirect_to [:admin, @newsletter], notice: "[#{label}] Newsletter was successfully delivered."
61+
end
62+
4863
private
4964

5065
# Use callbacks to share common setup or constraints between actions.
@@ -56,4 +71,6 @@ def set_newsletter
5671
def newsletter_params
5772
params.fetch(:newsletter, {}).permit(:title, :content)
5873
end
74+
75+
def deliver_live? = params[:live] == "true"
5976
end

app/jobs/notifications/event_job.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ class Notifications::EventJob < ApplicationJob
22
queue_as :default
33

44
def perform(event)
5+
event.deliver_notifications_in_bulk
6+
57
# Enqueue individual deliveries
68
event.notifications.each do |notification|
79
notification.transaction do

app/mailers/application_mailer.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ class ApplicationMailer < ActionMailer::Base
99
default from: email_address_with_name(FROM_ADDRESS, FROM_NAME)
1010
layout "emails/mailer"
1111

12-
def self.test_recipient_email
13-
Rails.configuration.settings.emails.test_recipient
12+
def self.test_recipients
13+
[Rails.configuration.settings.emails.test_recipient].compact
1414
end
1515

1616
def support_email
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
class Emails::NewsletterMailer < ApplicationMailer
4+
NEWSLETTER_EMAIL = Rails.configuration.settings.emails.broadcast_from_address
5+
6+
default from: email_address_with_name(
7+
Rails.configuration.settings.emails.broadcast_from_address,
8+
"Ross from Joy of Rails"
9+
)
10+
11+
def newsletter(newsletter:, user:, unsubscribe_token:)
12+
@newsletter = newsletter
13+
@user = user
14+
@unsubscribe_token = unsubscribe_token
15+
16+
headers["MESSAGE-STREAM"] = "broadcast"
17+
18+
mail(to: user.email, subject: newsletter.title)
19+
end
20+
end

app/models/notification_event.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def self.dump(data)
1414
end
1515

1616
has_many :notifications, dependent: :delete_all
17+
has_many :recipients, through: :notifications, source: :recipient, source_type: "User"
1718

1819
accepts_nested_attributes_for :notifications
1920

@@ -69,7 +70,11 @@ def recipient_attributes_for(recipient)
6970
}
7071
end
7172

73+
def deliver_notifications_in_bulk
74+
# no op
75+
end
76+
7277
def deliver_notification(notification)
73-
raise NotImplementedError, "Subclasses must implement #deliver_notification"
78+
# no op
7479
end
7580
end

app/models/user.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ class User < ApplicationRecord
88

99
has_one :newsletter_subscription, as: :subscriber, dependent: :destroy
1010

11+
scope :confirmed, -> { where.not(confirmed_at: nil) }
1112
scope :recently_confirmed, -> { where("confirmed_at > ?", 2.weeks.ago) }
13+
scope :subscribers, -> { confirmed.joins(:newsletter_subscription) }
1214

1315
accepts_nested_attributes_for :email_exchanges, limit: 1
1416

app/notifiers/newsletter_notifier.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
class NewsletterNotifier < NotificationEvent
2+
def self.deliver_to(users, newsletter:, **)
3+
new(params: {newsletter_id: newsletter.id}).deliver(users, **)
4+
end
5+
6+
def deliver_notifications_in_bulk
7+
newsletter = Newsletter.find(params[:newsletter_id])
8+
messages = build_newsletter_messages(newsletter)
9+
10+
PostmarkClient.deliver_messages(messages)
11+
12+
newsletter.touch(:sent_at)
13+
end
14+
15+
private
16+
17+
def postmark_client
18+
@postmark_client ||= Postmark::ApiClient.new(Rails.configuration.settings.postmark_api_token)
19+
end
20+
21+
def build_newsletter_messages(newsletter)
22+
recipients.find_each.filter_map do |user|
23+
if !user.confirmed?
24+
log_skip(newsletter, user, "user not confirmed")
25+
next
26+
end
27+
28+
unsubscribe_token = user&.newsletter_subscription&.generate_token_for(:unsubscribe)
29+
30+
if !unsubscribe_token
31+
log_skip(newsletter, user, "could not generate unsubscribe token")
32+
next
33+
end
34+
35+
Emails::NewsletterMailer.newsletter(newsletter:, user:, unsubscribe_token:)
36+
end
37+
end
38+
39+
def log_skip(newsletter, user, reason)
40+
Rails.logger.info "#[#{self.class}] Skipping delivery of newsletter #{newsletter.id} for: #{user.class.name}##{user.id}#{reason}"
41+
end
42+
end

app/views/admin/newsletters/show.html.erb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
<%= render Pages::Header.new(title: "Admin: #{@newsletter.title}") %>
22
<div class="section-content container py-gap">
3+
<div class="flex gap-x-2">
4+
<%= button_to "Send Test", deliver_admin_newsletter_path(@newsletter), class: "button secondary" %>
5+
<%= button_to "Send Live", deliver_admin_newsletter_path(@newsletter, live: true), class: "button primary" %>
6+
</div>
7+
38
<%= render @newsletter %>
49

510
<div>

app/views/components/markdown/base.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ def visit(node)
8686
html_block(node.to_html(options: @options))
8787
in :html_inline # This is a raw HTML inline element, so we skip here in safe mode
8888
html_inline(node.to_html(options: @options))
89+
in :strikethrough
90+
s { visit_children(node) }
8991
end
9092
end
9193

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<%= render Markdown::Base.new(@newsletter.content.html_safe) %>

0 commit comments

Comments
 (0)