Skip to content

Commit d95ad4e

Browse files
Merge pull request #2279 from NCCE/2897-primary-emails---sending-job
Strapi Email Sender Job
2 parents ebe4958 + 74f16c3 commit d95ad4e

File tree

20 files changed

+261
-20
lines changed

20 files changed

+261
-20
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
module Admin
2+
class SentEmailsController < Admin::ApplicationController
3+
# Overwrite any of the RESTful controller actions to implement custom behavior
4+
# For example, you may want to send an email after a foo is updated.
5+
#
6+
# def update
7+
# super
8+
# send_foo_updated_email(requested_resource)
9+
# end
10+
11+
# Override this method to specify custom lookup behavior.
12+
# This will be used to set the resource for the `show`, `edit`, and `update`
13+
# actions.
14+
#
15+
# def find_resource(param)
16+
# Foo.find_by!(slug: param)
17+
# end
18+
19+
# The result of this lookup will be available as `requested_resource`
20+
21+
# Override this if you have certain roles that require a subset
22+
# this will be used to set the records shown on the `index` action.
23+
#
24+
# def scoped_resource
25+
# if current_user.super_admin?
26+
# resource_class
27+
# else
28+
# resource_class.with_less_stuff
29+
# end
30+
# end
31+
32+
# Override `resource_params` if you want to transform the submitted
33+
# data before it's persisted. For example, the following would turn all
34+
# empty values into nil values. It uses other APIs such as `resource_class`
35+
# and `dashboard`:
36+
#
37+
# def resource_params
38+
# params.require(resource_class.model_name.param_key).
39+
# permit(dashboard.permitted_attributes).
40+
# transform_values { |value| value == "" ? nil : value }
41+
# end
42+
43+
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
44+
# for more information
45+
end
46+
end
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
class SentEmailDashboard < BaseDashboard
2+
# ATTRIBUTE_TYPES
3+
# a hash that describes the type of each of the model's fields.
4+
#
5+
# Each different type represents an Administrate::Field object,
6+
# which determines how the attribute is displayed
7+
# on pages throughout the dashboard.
8+
ATTRIBUTE_TYPES = {
9+
id: Field::String,
10+
user: Field::BelongsTo,
11+
subject: Field::String,
12+
mailer_type: Field::String,
13+
created_at: FORMATTED_DATE_TIME
14+
}.freeze
15+
16+
# COLLECTION_ATTRIBUTES
17+
# an array of attributes that will be displayed on the model's index page.
18+
COLLECTION_ATTRIBUTES = %i[
19+
created_at
20+
user
21+
subject
22+
].freeze
23+
24+
# SHOW_PAGE_ATTRIBUTES
25+
# an array of attributes that will be displayed on the model's show page.
26+
SHOW_PAGE_ATTRIBUTES = %i[
27+
user
28+
subject
29+
created_at
30+
].freeze
31+
32+
# FORM_ATTRIBUTES
33+
# an array of attributes that will be displayed
34+
# on the model's form (`new` and `edit`) pages.
35+
FORM_ATTRIBUTES = %i[].freeze
36+
37+
# COLLECTION_FILTERS
38+
# a hash that defines filters that can be used while searching via the search
39+
# field of the dashboard.
40+
#
41+
# For example to add an option to search for open resources by typing "open:"
42+
# in the search field:
43+
#
44+
# COLLECTION_FILTERS = {
45+
# open: ->(resources) { resources.where(open: true) }
46+
# }.freeze
47+
COLLECTION_FILTERS = {}.freeze
48+
49+
# Overwrite this method to customize how support_audits are displayed
50+
# across all pages of the admin dashboard.
51+
#
52+
# def display_resource()
53+
# end
54+
end

app/dashboards/user_dashboard.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ class UserDashboard < BaseDashboard
2626
stem_achiever_organisation_no: Field::String,
2727
future_learn_organisation_memberships: Field::Text,
2828
forgotten: Field::Boolean,
29-
audits: Field::HasMany.with_options(sort_by: "created_at", direction: "desc")
29+
audits: Field::HasMany.with_options(sort_by: "created_at", direction: "desc"),
30+
sent_emails: Field::HasMany.with_options(sort_by: "created_at", direction: "desc")
3031
}.freeze
3132

3233
# COLLECTION_ATTRIBUTES
@@ -59,6 +60,7 @@ class UserDashboard < BaseDashboard
5960
achievements
6061
teacher_reference_number
6162
assessment_attempts
63+
sent_emails
6264
].freeze
6365

6466
# FORM_ATTRIBUTES

app/jobs/send_cms_emails_job.rb

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class SendCmsEmailsJob < ApplicationJob
2+
PER_PAGE = 50
3+
def perform
4+
email_templates = Cms::Collections::EmailTemplate.all_records
5+
email_templates.each { process_template(_1) }
6+
end
7+
8+
def process_template(template)
9+
# Do Query
10+
data = template.template
11+
users = Programmes::ProgressQuery.new(data.programme, data.activity_state, data.enrolled, data.completed_programme_activity_groups).call
12+
13+
users.each do |user|
14+
CmsMailer.with(template_slug: data.slug, user_id: user.id).send_template.deliver_later
15+
end
16+
end
17+
end

app/mailers/cms_mailer.rb

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
class CmsMailer < ApplicationMailer
22
def send_template
3-
template_slug = params[:template_slug]
3+
@template_slug = params[:template_slug]
44
@user = User.find(params[:user_id])
5-
@template = Cms::Collections::EmailTemplate.get(template_slug).template
65

7-
@subject = @template.subject(@user)
6+
begin
7+
@template = Cms::Collections::EmailTemplate.get(@template_slug).template
88

9-
mail(to: @user.email, subject: @subject)
9+
@subject = @template.subject(@user)
10+
11+
mail(to: @user.email, subject: @subject)
12+
rescue ActiveRecord::RecordNotFound
13+
Sentry.capture_message("Failed to load the email template #{@template_slug}")
14+
end
1015
end
1116
end

app/services/cms/collections/email_template.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module Collections
33
class EmailTemplate < Resource
44
def self.is_collection = true
55

6-
def self.collection_attribute_mapping
6+
def self.collection_attribute_mappings
77
[
88
{model: Models::Collections::EmailTemplate, key: nil, param_name: :template}
99
]

app/services/cms/models/collections/email_template.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ module Cms
22
module Models
33
module Collections
44
class EmailTemplate
5-
attr_accessor :slug, :email_content, :programme, :completed_programme_activity_groups, :activity_state
5+
attr_accessor :slug, :email_content, :programme, :completed_programme_activity_groups, :activity_state, :enrolled
66

7-
def initialize(slug:, subject:, email_content:, programme_slug:, completed_programme_activity_group_slugs:, activity_state:)
7+
def initialize(slug:, subject:, email_content:, programme_slug:, completed_programme_activity_group_slugs:, activity_state:, enrolled:)
88
@slug = slug
99
@subject = subject
1010
@email_content = email_content
@@ -17,6 +17,7 @@ def initialize(slug:, subject:, email_content:, programme_slug:, completed_progr
1717
[]
1818
end
1919
@activity_state = activity_state
20+
@enrolled = enrolled
2021
end
2122

2223
def subject(user)

app/services/cms/providers/strapi/factories/model_factory.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ def self.to_email_template(strapi_data, _all_data)
8484
programme_slug: strapi_data[:programme][:data][:attributes][:slug],
8585
email_content: strapi_data[:emailContent].map { ComponentFactory.process_component(_1) }.compact,
8686
completed_programme_activity_group_slugs: strapi_data.dig(:completedGroupings, :data).nil? ? nil : strapi_data[:completedGroupings][:data].collect { _1[:attributes][:slug] },
87-
activity_state: strapi_data[:activityState]
87+
activity_state: strapi_data[:activityState],
88+
enrolled: strapi_data[:enrolled]
8889
}
8990
end
9091

app/services/cms/providers/strapi/mocks/collections/email_template.rb

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class EmailTemplate < StrapiMock
88
attribute(:slug) { Faker::Internet.slug }
99
attribute(:emailContent) { [] }
1010
attribute(:activityState) { "active" }
11+
attribute(:enrolled) { true }
1112
attribute(:programme) {
1213
{data: {attributes: {slug: "primary-certificate"}}}
1314
}

config/environments/development.rb

+3
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@
110110
config.view_component.preview_route = "/rails/components"
111111
config.view_component.generate.sidecar = true
112112

113+
config.action_mailer.default_url_options = {host: "teachcomputing.rpfdev.com"}
114+
routes.default_url_options = {host: "teachcomputing.rpfdev.com"}
115+
113116
config.lograge.enabled = true
114117
config.lograge.ignore_actions = [Healthcheck::CONTROLLER_ACTION]
115118
config.lograge.custom_options = lambda do |event|

config/routes.rb

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
post :report_results
4343
end
4444
end
45+
resources :sent_emails, only: %i[index show]
4546
end
4647

4748
namespace :api do

erd.pdf

0 Bytes
Binary file not shown.

spec/jobs/send_cms_emails_job_spec.rb

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
require "rails_helper"
2+
3+
RSpec.describe SendCmsEmailsJob, type: :job do
4+
let(:email_template_1_slug) { "send-cms-email-template-1-slug" }
5+
let(:email_template_2_slug) { "send-cms-email-template-2-slug" }
6+
let(:users) { create_list(:user, 2) }
7+
8+
let(:email_template_1) {
9+
Cms::Mocks::Collections::EmailTemplate.generate_raw_data(slug: email_template_1_slug)
10+
}
11+
let(:email_template_2) {
12+
Cms::Mocks::Collections::EmailTemplate.generate_raw_data(slug: email_template_2_slug)
13+
}
14+
15+
before do
16+
stub_strapi_email_template(email_template_1_slug, email_template: email_template_1)
17+
stub_strapi_email_template(email_template_2_slug, email_template: email_template_2)
18+
stub_strapi_email_templates(email_templates: [email_template_1, email_template_2], page: 1, page_size: 50)
19+
20+
allow_any_instance_of(Programmes::ProgressQuery).to receive(:call).and_return(users)
21+
end
22+
23+
it "should enqueue both templates" do
24+
expect do
25+
described_class.perform_now
26+
end.to have_enqueued_mail(CmsMailer, :send_template).with(a_hash_including(params: {user_id: users.first.id, template_slug: email_template_1_slug}))
27+
.and have_enqueued_mail(CmsMailer, :send_template).with(a_hash_including(params: {user_id: users.first.id, template_slug: email_template_2_slug}))
28+
.and have_enqueued_mail(CmsMailer, :send_template).with(a_hash_including(params: {user_id: users.second.id, template_slug: email_template_1_slug}))
29+
.and have_enqueued_mail(CmsMailer, :send_template).with(a_hash_including(params: {user_id: users.second.id, template_slug: email_template_2_slug}))
30+
end
31+
end

spec/mailers/cms_mailer_spec.rb

+23-7
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515
Cms::Mocks::EmailComponents::Text.generate_raw_data(
1616
text_content: [
1717
{
18-
type: :paragraph,
18+
type: "paragraph",
1919
children: [
20-
{type: :text, text: "Hello {first_name}"}
20+
{type: "text", text: "Hello {first_name}"}
2121
]
2222
},
2323
{
24-
type: :paragraph,
24+
type: "paragraph",
2525
children: [
26-
{type: :text, text: "You completed {last_cpd_title}"}
26+
{type: "text", text: "You completed {last_cpd_title}"}
2727
]
2828
},
2929
{
@@ -72,7 +72,7 @@
7272

7373
describe "send_template" do
7474
before do
75-
@mail = CmsMailer.with(user_id: user.id, template_slug: slug).send_template
75+
@mail = described_class.with(user_id: user.id, template_slug: slug).send_template
7676
end
7777

7878
it "renders the headers" do
@@ -131,7 +131,7 @@
131131
before do
132132
travel_to 1.day.from_now do
133133
create(:completed_achievement, activity: second_activity, user:)
134-
@future_mail = CmsMailer.with(user_id: user.id, template_slug: slug).send_template
134+
@future_mail = described_class.with(user_id: user.id, template_slug: slug).send_template
135135
end
136136
end
137137

@@ -147,7 +147,7 @@
147147

148148
describe "send_template with merged subject" do
149149
before do
150-
@mail = CmsMailer.with(user_id: user.id, template_slug: slug_with_merge_subject).send_template
150+
@mail = described_class.with(user_id: user.id, template_slug: slug_with_merge_subject).send_template
151151
end
152152

153153
it "renders the headers" do
@@ -156,4 +156,20 @@
156156
expect(@mail.from).to eq(["[email protected]"])
157157
end
158158
end
159+
160+
describe "with missing template" do
161+
let(:missing_slug) { "missing_template_slug" }
162+
163+
before do
164+
stub_strapi_email_template_missing(missing_slug)
165+
allow(Sentry).to receive(:capture_message)
166+
end
167+
168+
it "logs error to sentry" do
169+
described_class.with(user_id: user.id, template_slug: missing_slug).send_template.deliver_now
170+
expect(Sentry)
171+
.to have_received(:capture_message)
172+
.with("Failed to load the email template #{missing_slug}")
173+
end
174+
end
159175
end
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require "rails_helper"
2+
3+
RSpec.describe "Admin::SentEmailsController" do
4+
let!(:sent_email) { create(:sent_email) }
5+
6+
before do
7+
allow_any_instance_of(Admin::ApplicationController).to receive(:authenticate_admin).and_return("[email protected]")
8+
end
9+
10+
describe "GET #index" do
11+
before do
12+
get admin_sent_emails_path
13+
end
14+
15+
it "should render correct template" do
16+
expect(response).to render_template("index")
17+
end
18+
end
19+
20+
describe "GET #show" do
21+
before do
22+
get admin_sent_email_path(sent_email)
23+
end
24+
25+
it "should render correct template" do
26+
expect(response).to render_template("show")
27+
end
28+
end
29+
end

spec/services/cms/models/collections/email_template_spec.rb

+16-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
email_content: Cms::Mocks::EmailComponents::Text.generate_raw_data(text_content:),
4747
programme_slug: "primary-certificate",
4848
completed_programme_activity_group_slugs: [],
49-
activity_state: :active
49+
activity_state: :active,
50+
enrolled: true
5051
)
5152
end
5253

@@ -83,4 +84,18 @@
8384
expect(@model.time_diff_words(25.months.ago)).to eq("2 years")
8485
end
8586
end
87+
88+
it "passing nil into completed_programme_activity_group_slugs defaults to array" do
89+
@model = described_class.new(
90+
slug:,
91+
subject:,
92+
email_content: Cms::Mocks::EmailComponents::Text.generate_raw_data(text_content:),
93+
programme_slug: "primary-certificate",
94+
completed_programme_activity_group_slugs: nil,
95+
activity_state: :active,
96+
enrolled: true
97+
)
98+
99+
expect(@model.completed_programme_activity_groups).to eq([])
100+
end
86101
end

spec/services/cms/models/email_components/base_component_spec.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
email_content: Cms::Mocks::Text::RichBlocks.generate_data,
1111
programme_slug: programme.slug,
1212
completed_programme_activity_group_slugs: [],
13-
activity_state: :active
13+
activity_state: :active,
14+
enrolled: true
1415
)
1516
}
1617

0 commit comments

Comments
 (0)