Skip to content

Commit

Permalink
Add custom emails to admin area (forem#21340)
Browse files Browse the repository at this point in the history
* Add custom emails to admin area

* Fix i18n

* Adjust newsletters

* Adjust newsletters

* Newsletter i18n

* Newsletter i18n

* Fix email spec

* Get headers working

* Get headers working
  • Loading branch information
benhalpern authored Oct 25, 2024
1 parent 88336eb commit f87ca82
Show file tree
Hide file tree
Showing 25 changed files with 501 additions and 1 deletion.
36 changes: 36 additions & 0 deletions app/controllers/admin/emails_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Admin
class EmailsController < Admin::ApplicationController
layout "admin"

def index
@emails = Email.page(params[:page] || 1).includes([:audience_segment]).order("id DESC").per(25)
end

def new
@audience_segments = AudienceSegment.all
@email = Email.new
end

def show
@email = Email.find(params[:id])
end

def create
@email = Email.new(email_params)
if @email.save
flash[:success] = I18n.t("admin.emails_controller.created")
redirect_to admin_email_path(@email.id)
else
@audience_segments = AudienceSegment.all
flash[:danger] = @email.errors_as_sentence
render :new
end
end

private

def email_params
params.permit(:subject, :body, :audience_segment_id)
end
end
end
18 changes: 18 additions & 0 deletions app/mailers/custom_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class CustomMailer < ApplicationMailer
default from: -> { email_from(I18n.t("mailers.custom_mailer.from")) }

def custom_email
@user = params[:user]
@content = params[:content]
@unsubscribe = generate_unsubscribe_token(@user.id, :email_newsletter)

# set sendgrid category in the header using smtp api
# https://docs.sendgrid.com/for-developers/sending-email/building-an-x-smtpapi-header
if ForemInstance.sendgrid_enabled?
smtpapi_header = { category: "Custom Email" }.to_json
headers["X-SMTPAPI"] = smtpapi_header
end

mail(to: @user.email, subject: params[:subject])
end
end
1 change: 1 addition & 0 deletions app/models/admin_menu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class AdminMenu
item(name: "organizations"),
item(name: "podcasts"),
item(name: "tags"),
item(name: "emails"),
]

scope :customization, "tools-line", [
Expand Down
1 change: 1 addition & 0 deletions app/models/audience_segment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class AudienceSegment < ApplicationRecord

has_many :segmented_users, dependent: :destroy
has_many :users, through: :segmented_users
has_many :emails, dependent: :nullify

after_validation :persist_recently_active_users, unless: :manual?

Expand Down
26 changes: 26 additions & 0 deletions app/models/email.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class Email < ApplicationRecord
belongs_to :audience_segment, optional: true

after_create :deliver_to_users

validates :subject, presence: true
validates :body, presence: true

BATCH_SIZE = Rails.env.production? ? 1000 : 10

def deliver_to_users
user_scope = if audience_segment
audience_segment.users.registered.joins(:notification_setting)
.where(notification_setting: { email_newsletter: true })
.where.not(email: "")
else
User.registered.joins(:notification_setting)
.where(notification_setting: { email_newsletter: true })
.where.not(email: "")
end

user_scope.find_in_batches(batch_size: BATCH_SIZE) do |users_batch|
Emails::BatchCustomSendWorker.perform_async(users_batch.map(&:id), subject, body)
end
end
end
46 changes: 46 additions & 0 deletions app/views/admin/emails/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<div class="grid l:grid-cols-2 gap-6 mb-4">
<div class="flex flex-col gap-4">

<div class="crayons-field">
<%= label_tag :audience_segment_id, "Audience Segment:", class: "crayons-field__label" %>
<%= select_tag :audience_segment_id, options_for_select([["Entire list", nil]] + @audience_segments.map { |s| [s.name || s.type_of, s.id]}), class: "crayons-textfield", autocomplete: "off" %>
</div>

<div class="crayons-field">
<%= label_tag :subject, "Subject:", class: "crayons-field__label" %>
<%= text_field_tag :subject, @email.subject, class: "crayons-textfield", autocomplete: "off" %>
</div>

<div class="crayons-field">
<%= label_tag :body, "Body Content:", class: "crayons-field__label" %>
<%= text_area_tag :body, @email.body, size: "100x5", class: "crayons-textfield" %>
</div>
</div>

<div>
<div class="crayons-card crayons-card--secondary">
<% if @email.persisted? %>
<h2 class="crayons-title mb-2">Preview</h2>
<p><strong>Subject:</strong> <%= @email.subject %></p>
<p><strong>Body:</strong></p>
<div class="crayons-article__body text-styles">
<%= simple_format(@email.body) %>
</div>
<% else %>
<div class="flex flex-col gap-3">
<p>
Use this form to compose a new email. Fill in the subject, body, and specify the recipients.
</p>
<p>
You can choose to send the email immediately or schedule it for a later time.
</p>
<p>
Select the appropriate recipient group or provide a custom list of email addresses.
</p>
</div>
<% end %>
</div>
</div>
</div>

<%#= javascript_include_tag "admin/emails", defer: true %>
56 changes: 56 additions & 0 deletions app/views/admin/emails/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<h1 class="crayons-title mb-3">Emails</h1>

<div
data-controller="confirmation-modal"
data-confirmation-modal-root-selector-value="#confirmation-modal-root"
data-confirmation-modal-content-selector-value="#confirmation-modal"
data-confirmation-modal-title-value="Confirm changes"
data-confirmation-modal-size-value="m">

<nav class="flex mb-4" aria-label="Emails navigation">
<%= form_tag(admin_emails_path, method: "get") do %>
<%= text_field_tag(:search, params[:search], aria: { label: "Search" }, class: "crayons-header--search-input crayons-textfield", placeholder: "Search", autocomplete: "off") %>
<% end %>
<div class="ml-auto">
<div class="justify-end">
<%= link_to "Compose New Email", new_admin_email_path, class: "crayons-btn" %>
</div>
</div>
</nav>

<%= paginate @emails %>


<table class="crayons-table" width="100%">
<thead>
<tr>
<th scope="col">Subject</th>
<th scope="col">Sent At</th>
<th scope="col">Segment</th>
<th scope="col">Body</th>
</tr>
</thead>
<tbody class="crayons-card">
<% @emails.each do |email| %>
<tr data-row-id="<%= email.id %>">
<td><%= link_to email.subject, admin_email_path(email) %></td>
<td><%= email.created_at %></td>
<td><%= email.audience_segment&.name || email.audience_segment&.type_of || "All" %></td>
<td><%= truncate(email.body, length: 100) %></td>
<td><%= link_to "Details", admin_email_path(email), class: "crayons-btn" %></td>
<td>
<button
class="crayons-btn crayons-btn--danger"
data-item-id="<%= email.id %>"
data-endpoint="/admin/emails"
data-username="<%= current_user.username %>"
data-action="click->confirmation-modal#openModal">Destroy</button>
</td>
</tr>
<% end %>
</tbody>
</table>

<%= render partial: "admin/shared/destroy_confirmation_modal" %>
<%= paginate @emails %>
</div>
7 changes: 7 additions & 0 deletions app/views/admin/emails/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<h1 class="crayons-title mb-4">Make a new Email</h1>
<div class="crayons-card p-6">
<%= form_for([:admin, @email], url: admin_emails_path, method: :post) do %>
<%= render "form" %>
<%= submit_tag "Send Billboard", class: "c-btn c-btn--primary" %>
<% end %>
</div>
4 changes: 4 additions & 0 deletions app/views/admin/emails/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<h1 class="crayons-title mb-4"><%= @email.subject %></h1>
<div class="crayons-card p-6">
<%= @email.body %>
</div>
1 change: 1 addition & 0 deletions app/views/mailers/custom_mailer/custom_email.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= @content.html_safe %>
13 changes: 13 additions & 0 deletions app/workers/emails/batch_custom_send_worker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Emails
class BatchCustomSendWorker
include Sidekiq::Job

sidekiq_options queue: :medium_priority, retry: 15

def perform(user_ids, subject, content)
user_ids.each do |id|
CustomMailer.with(user: User.find(id), subject: subject, content: subject).custom_email.deliver_now
end
end
end
end
5 changes: 5 additions & 0 deletions config/locales/controllers/admin/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ en:
deleted: Billboard has been deleted!
updated: Billboard has been updated!
wrong: Something went wrong with deleting the Billboard.
emails_controller:
created: Emails are being sent!
deleted: Email has been deleted!
updated: Email has been updated!
wrong: Something went wrong with deleting the Email.
gdpr_delete_requests_controller:
deleted: Successfully marked as deleted
extensions_controller:
Expand Down
5 changes: 5 additions & 0 deletions config/locales/controllers/admin/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ fr:
deleted: L'annonce a été supprimée !
updated: L'annonce publicitaire a été mise à jour !
wrong: Quelque chose s'est mal passé avec la suppression de l'annonce publicitaire.
emails_controller:
created: Emails are being sent!
deleted: Email has been deleted!
updated: Email has been updated!
wrong: Something went wrong with deleting the Email.
extensions_controller:
update_success: Les extensions ont été mises à jour.
gdpr_delete_requests_controller:
Expand Down
2 changes: 2 additions & 0 deletions config/locales/mailers/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ en:
other_community_posts: other %{community} posts you might like
other_top_posts: other top %{community} posts
other_trending_posts: other trending %{community} posts
custom_mailer:
from: Newsletter
notify_mailer:
account_deleted: "%{community} - Account Deletion Confirmation"
deletion_requested: "%{community} - Account Deletion Requested"
Expand Down
2 changes: 2 additions & 0 deletions config/locales/mailers/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ fr:
other_community_posts: d'autres posts de la %{community} que vous pourriez aimer
other_top_posts: d'autres articles tendance de la %{community}
other_trending_posts: d'autres articles tendance de la %{community}.
custom_mailer:
from: Newsletter
notify_mailer:
account_deleted: "%{community} - Confirmation de la suppression du compte"
deletion_requested: "%{community} - Suppression du compte demandée"
Expand Down
1 change: 1 addition & 0 deletions config/routes/admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
patch "update_org_credits"
end
end
resources :emails
resources :podcasts, only: %i[index edit update destroy] do
member do
post :fetch
Expand Down
10 changes: 10 additions & 0 deletions db/migrate/20241018175616_create_emails.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CreateEmails < ActiveRecord::Migration[7.0]
def change
create_table :emails do |t|
t.string :subject, null: false
t.text :body, null: false
t.references :audience_segment, foreign_key: true
t.timestamps
end
end
end
5 changes: 5 additions & 0 deletions db/migrate/20241023131302_add_name_to_audience_segments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddNameToAudienceSegments < ActiveRecord::Migration[7.0]
def change
add_column :audience_segments, :name, :string
end
end
13 changes: 12 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2024_09_09_155030) do
ActiveRecord::Schema[7.0].define(version: 2024_10_23_131302) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "ltree"
Expand Down Expand Up @@ -184,6 +184,7 @@

create_table "audience_segments", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "name"
t.integer "type_of"
t.datetime "updated_at", null: false
end
Expand Down Expand Up @@ -528,6 +529,15 @@
t.index ["user_id"], name: "index_email_authorizations_on_user_id"
end

create_table "emails", force: :cascade do |t|
t.bigint "audience_segment_id"
t.text "body", null: false
t.datetime "created_at", null: false
t.string "subject", null: false
t.datetime "updated_at", null: false
t.index ["audience_segment_id"], name: "index_emails_on_audience_segment_id"
end

create_table "feed_events", force: :cascade do |t|
t.bigint "article_id", null: false
t.integer "article_position"
Expand Down Expand Up @@ -1478,6 +1488,7 @@
add_foreign_key "display_ad_events", "users", on_delete: :cascade
add_foreign_key "display_ads", "organizations", on_delete: :cascade
add_foreign_key "email_authorizations", "users", on_delete: :cascade
add_foreign_key "emails", "audience_segments"
add_foreign_key "feed_events", "articles", on_delete: :cascade
add_foreign_key "feed_events", "users", on_delete: :nullify
add_foreign_key "feedback_messages", "users", column: "affected_id", on_delete: :nullify
Expand Down
6 changes: 6 additions & 0 deletions spec/factories/emails.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
FactoryBot.define do
factory :email do
subject { Faker::Lorem.sentence }
body { Faker::Lorem.sentence }
end
end
8 changes: 8 additions & 0 deletions spec/factories/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,5 +201,13 @@
.update_columns(email_newsletter: true, email_digest_periodic: true)
end
end

trait :without_newsletters do
after(:create) do |user|
Users::NotificationSetting.find_by(user_id: user.id)
.update_columns(email_newsletter: false, email_digest_periodic: true)
end
end

end
end
Loading

0 comments on commit f87ca82

Please sign in to comment.