diff --git a/app/controllers/admin/sent_emails_controller.rb b/app/controllers/admin/sent_emails_controller.rb new file mode 100644 index 0000000000..3b571a20a9 --- /dev/null +++ b/app/controllers/admin/sent_emails_controller.rb @@ -0,0 +1,46 @@ +module Admin + class SentEmailsController < Admin::ApplicationController + # Overwrite any of the RESTful controller actions to implement custom behavior + # For example, you may want to send an email after a foo is updated. + # + # def update + # super + # send_foo_updated_email(requested_resource) + # end + + # Override this method to specify custom lookup behavior. + # This will be used to set the resource for the `show`, `edit`, and `update` + # actions. + # + # def find_resource(param) + # Foo.find_by!(slug: param) + # end + + # The result of this lookup will be available as `requested_resource` + + # Override this if you have certain roles that require a subset + # this will be used to set the records shown on the `index` action. + # + # def scoped_resource + # if current_user.super_admin? + # resource_class + # else + # resource_class.with_less_stuff + # end + # end + + # Override `resource_params` if you want to transform the submitted + # data before it's persisted. For example, the following would turn all + # empty values into nil values. It uses other APIs such as `resource_class` + # and `dashboard`: + # + # def resource_params + # params.require(resource_class.model_name.param_key). + # permit(dashboard.permitted_attributes). + # transform_values { |value| value == "" ? nil : value } + # end + + # See https://administrate-prototype.herokuapp.com/customizing_controller_actions + # for more information + end +end diff --git a/app/dashboards/sent_email_dashboard.rb b/app/dashboards/sent_email_dashboard.rb new file mode 100644 index 0000000000..fadd135cb0 --- /dev/null +++ b/app/dashboards/sent_email_dashboard.rb @@ -0,0 +1,54 @@ +class SentEmailDashboard < BaseDashboard + # ATTRIBUTE_TYPES + # a hash that describes the type of each of the model's fields. + # + # Each different type represents an Administrate::Field object, + # which determines how the attribute is displayed + # on pages throughout the dashboard. + ATTRIBUTE_TYPES = { + id: Field::String, + user: Field::BelongsTo, + subject: Field::String, + mailer_type: Field::String, + created_at: FORMATTED_DATE_TIME + }.freeze + + # COLLECTION_ATTRIBUTES + # an array of attributes that will be displayed on the model's index page. + COLLECTION_ATTRIBUTES = %i[ + created_at + user + subject + ].freeze + + # SHOW_PAGE_ATTRIBUTES + # an array of attributes that will be displayed on the model's show page. + SHOW_PAGE_ATTRIBUTES = %i[ + user + subject + created_at + ].freeze + + # FORM_ATTRIBUTES + # an array of attributes that will be displayed + # on the model's form (`new` and `edit`) pages. + FORM_ATTRIBUTES = %i[].freeze + + # COLLECTION_FILTERS + # a hash that defines filters that can be used while searching via the search + # field of the dashboard. + # + # For example to add an option to search for open resources by typing "open:" + # in the search field: + # + # COLLECTION_FILTERS = { + # open: ->(resources) { resources.where(open: true) } + # }.freeze + COLLECTION_FILTERS = {}.freeze + + # Overwrite this method to customize how support_audits are displayed + # across all pages of the admin dashboard. + # + # def display_resource() + # end +end diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb index df58c24c2b..9dea894906 100644 --- a/app/dashboards/user_dashboard.rb +++ b/app/dashboards/user_dashboard.rb @@ -26,7 +26,8 @@ class UserDashboard < BaseDashboard stem_achiever_organisation_no: Field::String, future_learn_organisation_memberships: Field::Text, forgotten: Field::Boolean, - audits: Field::HasMany.with_options(sort_by: "created_at", direction: "desc") + audits: Field::HasMany.with_options(sort_by: "created_at", direction: "desc"), + sent_emails: Field::HasMany.with_options(sort_by: "created_at", direction: "desc") }.freeze # COLLECTION_ATTRIBUTES @@ -59,6 +60,7 @@ class UserDashboard < BaseDashboard achievements teacher_reference_number assessment_attempts + sent_emails ].freeze # FORM_ATTRIBUTES diff --git a/app/jobs/send_cms_emails_job.rb b/app/jobs/send_cms_emails_job.rb new file mode 100644 index 0000000000..fe409403dc --- /dev/null +++ b/app/jobs/send_cms_emails_job.rb @@ -0,0 +1,17 @@ +class SendCmsEmailsJob < ApplicationJob + PER_PAGE = 50 + def perform + email_templates = Cms::Collections::EmailTemplate.all_records + email_templates.each { process_template(_1) } + end + + def process_template(template) + # Do Query + data = template.template + users = Programmes::ProgressQuery.new(data.programme, data.activity_state, data.enrolled, data.completed_programme_activity_groups).call + + users.each do |user| + CmsMailer.with(template_slug: data.slug, user_id: user.id).send_template.deliver_later + end + end +end diff --git a/app/mailers/cms_mailer.rb b/app/mailers/cms_mailer.rb index ebf832a810..ac68acd86b 100644 --- a/app/mailers/cms_mailer.rb +++ b/app/mailers/cms_mailer.rb @@ -1,11 +1,16 @@ class CmsMailer < ApplicationMailer def send_template - template_slug = params[:template_slug] + @template_slug = params[:template_slug] @user = User.find(params[:user_id]) - @template = Cms::Collections::EmailTemplate.get(template_slug).template - @subject = @template.subject(@user) + begin + @template = Cms::Collections::EmailTemplate.get(@template_slug).template - mail(to: @user.email, subject: @subject) + @subject = @template.subject(@user) + + mail(to: @user.email, subject: @subject) + rescue ActiveRecord::RecordNotFound + Sentry.capture_message("Failed to load the email template #{@template_slug}") + end end end diff --git a/app/services/cms/collections/email_template.rb b/app/services/cms/collections/email_template.rb index d63336af38..bb4262d274 100644 --- a/app/services/cms/collections/email_template.rb +++ b/app/services/cms/collections/email_template.rb @@ -3,7 +3,7 @@ module Collections class EmailTemplate < Resource def self.is_collection = true - def self.collection_attribute_mapping + def self.collection_attribute_mappings [ {model: Models::Collections::EmailTemplate, key: nil, param_name: :template} ] diff --git a/app/services/cms/models/collections/email_template.rb b/app/services/cms/models/collections/email_template.rb index 8774d620be..5a32ce4964 100644 --- a/app/services/cms/models/collections/email_template.rb +++ b/app/services/cms/models/collections/email_template.rb @@ -2,9 +2,9 @@ module Cms module Models module Collections class EmailTemplate - attr_accessor :slug, :email_content, :programme, :completed_programme_activity_groups, :activity_state + attr_accessor :slug, :email_content, :programme, :completed_programme_activity_groups, :activity_state, :enrolled - def initialize(slug:, subject:, email_content:, programme_slug:, completed_programme_activity_group_slugs:, activity_state:) + def initialize(slug:, subject:, email_content:, programme_slug:, completed_programme_activity_group_slugs:, activity_state:, enrolled:) @slug = slug @subject = subject @email_content = email_content @@ -17,6 +17,7 @@ def initialize(slug:, subject:, email_content:, programme_slug:, completed_progr [] end @activity_state = activity_state + @enrolled = enrolled end def subject(user) diff --git a/app/services/cms/providers/strapi/factories/model_factory.rb b/app/services/cms/providers/strapi/factories/model_factory.rb index cc8452ff5f..11f474c9f2 100644 --- a/app/services/cms/providers/strapi/factories/model_factory.rb +++ b/app/services/cms/providers/strapi/factories/model_factory.rb @@ -84,7 +84,8 @@ def self.to_email_template(strapi_data, _all_data) programme_slug: strapi_data[:programme][:data][:attributes][:slug], email_content: strapi_data[:emailContent].map { ComponentFactory.process_component(_1) }.compact, completed_programme_activity_group_slugs: strapi_data.dig(:completedGroupings, :data).nil? ? nil : strapi_data[:completedGroupings][:data].collect { _1[:attributes][:slug] }, - activity_state: strapi_data[:activityState] + activity_state: strapi_data[:activityState], + enrolled: strapi_data[:enrolled] } end diff --git a/app/services/cms/providers/strapi/mocks/collections/email_template.rb b/app/services/cms/providers/strapi/mocks/collections/email_template.rb index 7e3f530a59..dffafdc50d 100644 --- a/app/services/cms/providers/strapi/mocks/collections/email_template.rb +++ b/app/services/cms/providers/strapi/mocks/collections/email_template.rb @@ -8,6 +8,7 @@ class EmailTemplate < StrapiMock attribute(:slug) { Faker::Internet.slug } attribute(:emailContent) { [] } attribute(:activityState) { "active" } + attribute(:enrolled) { true } attribute(:programme) { {data: {attributes: {slug: "primary-certificate"}}} } diff --git a/config/environments/development.rb b/config/environments/development.rb index 462e6b9f56..5b7a9539ce 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -110,6 +110,9 @@ config.view_component.preview_route = "/rails/components" config.view_component.generate.sidecar = true + config.action_mailer.default_url_options = {host: "teachcomputing.rpfdev.com"} + routes.default_url_options = {host: "teachcomputing.rpfdev.com"} + config.lograge.enabled = true config.lograge.ignore_actions = [Healthcheck::CONTROLLER_ACTION] config.lograge.custom_options = lambda do |event| diff --git a/config/routes.rb b/config/routes.rb index 3e6fe26881..1e2f9319e0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,6 +42,7 @@ post :report_results end end + resources :sent_emails, only: %i[index show] end namespace :api do diff --git a/erd.pdf b/erd.pdf index dc830cedf5..353144ed8e 100644 Binary files a/erd.pdf and b/erd.pdf differ diff --git a/spec/jobs/send_cms_emails_job_spec.rb b/spec/jobs/send_cms_emails_job_spec.rb new file mode 100644 index 0000000000..69212c4464 --- /dev/null +++ b/spec/jobs/send_cms_emails_job_spec.rb @@ -0,0 +1,31 @@ +require "rails_helper" + +RSpec.describe SendCmsEmailsJob, type: :job do + let(:email_template_1_slug) { "send-cms-email-template-1-slug" } + let(:email_template_2_slug) { "send-cms-email-template-2-slug" } + let(:users) { create_list(:user, 2) } + + let(:email_template_1) { + Cms::Mocks::Collections::EmailTemplate.generate_raw_data(slug: email_template_1_slug) + } + let(:email_template_2) { + Cms::Mocks::Collections::EmailTemplate.generate_raw_data(slug: email_template_2_slug) + } + + before do + stub_strapi_email_template(email_template_1_slug, email_template: email_template_1) + stub_strapi_email_template(email_template_2_slug, email_template: email_template_2) + stub_strapi_email_templates(email_templates: [email_template_1, email_template_2], page: 1, page_size: 50) + + allow_any_instance_of(Programmes::ProgressQuery).to receive(:call).and_return(users) + end + + it "should enqueue both templates" do + expect do + described_class.perform_now + end.to have_enqueued_mail(CmsMailer, :send_template).with(a_hash_including(params: {user_id: users.first.id, template_slug: email_template_1_slug})) + .and have_enqueued_mail(CmsMailer, :send_template).with(a_hash_including(params: {user_id: users.first.id, template_slug: email_template_2_slug})) + .and have_enqueued_mail(CmsMailer, :send_template).with(a_hash_including(params: {user_id: users.second.id, template_slug: email_template_1_slug})) + .and have_enqueued_mail(CmsMailer, :send_template).with(a_hash_including(params: {user_id: users.second.id, template_slug: email_template_2_slug})) + end +end diff --git a/spec/mailers/cms_mailer_spec.rb b/spec/mailers/cms_mailer_spec.rb index 11bff0e5ff..7c094c357c 100644 --- a/spec/mailers/cms_mailer_spec.rb +++ b/spec/mailers/cms_mailer_spec.rb @@ -15,15 +15,15 @@ Cms::Mocks::EmailComponents::Text.generate_raw_data( text_content: [ { - type: :paragraph, + type: "paragraph", children: [ - {type: :text, text: "Hello {first_name}"} + {type: "text", text: "Hello {first_name}"} ] }, { - type: :paragraph, + type: "paragraph", children: [ - {type: :text, text: "You completed {last_cpd_title}"} + {type: "text", text: "You completed {last_cpd_title}"} ] }, { @@ -72,7 +72,7 @@ describe "send_template" do before do - @mail = CmsMailer.with(user_id: user.id, template_slug: slug).send_template + @mail = described_class.with(user_id: user.id, template_slug: slug).send_template end it "renders the headers" do @@ -131,7 +131,7 @@ before do travel_to 1.day.from_now do create(:completed_achievement, activity: second_activity, user:) - @future_mail = CmsMailer.with(user_id: user.id, template_slug: slug).send_template + @future_mail = described_class.with(user_id: user.id, template_slug: slug).send_template end end @@ -147,7 +147,7 @@ describe "send_template with merged subject" do before do - @mail = CmsMailer.with(user_id: user.id, template_slug: slug_with_merge_subject).send_template + @mail = described_class.with(user_id: user.id, template_slug: slug_with_merge_subject).send_template end it "renders the headers" do @@ -156,4 +156,20 @@ expect(@mail.from).to eq(["noreply@teachcomputing.org"]) end end + + describe "with missing template" do + let(:missing_slug) { "missing_template_slug" } + + before do + stub_strapi_email_template_missing(missing_slug) + allow(Sentry).to receive(:capture_message) + end + + it "logs error to sentry" do + described_class.with(user_id: user.id, template_slug: missing_slug).send_template.deliver_now + expect(Sentry) + .to have_received(:capture_message) + .with("Failed to load the email template #{missing_slug}") + end + end end diff --git a/spec/requests/admin/sent_emails_spec.rb b/spec/requests/admin/sent_emails_spec.rb new file mode 100644 index 0000000000..8e099c6513 --- /dev/null +++ b/spec/requests/admin/sent_emails_spec.rb @@ -0,0 +1,29 @@ +require "rails_helper" + +RSpec.describe "Admin::SentEmailsController" do + let!(:sent_email) { create(:sent_email) } + + before do + allow_any_instance_of(Admin::ApplicationController).to receive(:authenticate_admin).and_return("user@example.com") + end + + describe "GET #index" do + before do + get admin_sent_emails_path + end + + it "should render correct template" do + expect(response).to render_template("index") + end + end + + describe "GET #show" do + before do + get admin_sent_email_path(sent_email) + end + + it "should render correct template" do + expect(response).to render_template("show") + end + end +end diff --git a/spec/services/cms/models/collections/email_template_spec.rb b/spec/services/cms/models/collections/email_template_spec.rb index 5aeb76ee11..f4e13c3f79 100644 --- a/spec/services/cms/models/collections/email_template_spec.rb +++ b/spec/services/cms/models/collections/email_template_spec.rb @@ -46,7 +46,8 @@ email_content: Cms::Mocks::EmailComponents::Text.generate_raw_data(text_content:), programme_slug: "primary-certificate", completed_programme_activity_group_slugs: [], - activity_state: :active + activity_state: :active, + enrolled: true ) end @@ -83,4 +84,18 @@ expect(@model.time_diff_words(25.months.ago)).to eq("2 years") end end + + it "passing nil into completed_programme_activity_group_slugs defaults to array" do + @model = described_class.new( + slug:, + subject:, + email_content: Cms::Mocks::EmailComponents::Text.generate_raw_data(text_content:), + programme_slug: "primary-certificate", + completed_programme_activity_group_slugs: nil, + activity_state: :active, + enrolled: true + ) + + expect(@model.completed_programme_activity_groups).to eq([]) + end end diff --git a/spec/services/cms/models/email_components/base_component_spec.rb b/spec/services/cms/models/email_components/base_component_spec.rb index 6738eda28f..c2793acb7c 100644 --- a/spec/services/cms/models/email_components/base_component_spec.rb +++ b/spec/services/cms/models/email_components/base_component_spec.rb @@ -10,7 +10,8 @@ email_content: Cms::Mocks::Text::RichBlocks.generate_data, programme_slug: programme.slug, completed_programme_activity_group_slugs: [], - activity_state: :active + activity_state: :active, + enrolled: true ) } diff --git a/spec/services/cms/models/email_components/course_list_spec.rb b/spec/services/cms/models/email_components/course_list_spec.rb index 7019f25311..a59c4cd149 100644 --- a/spec/services/cms/models/email_components/course_list_spec.rb +++ b/spec/services/cms/models/email_components/course_list_spec.rb @@ -10,7 +10,8 @@ email_content: Cms::Mocks::Text::RichBlocks.generate_data, programme_slug: programme.slug, completed_programme_activity_group_slugs: [], - activity_state: :active + activity_state: :active, + enrolled: true ) } let!(:activity1) { create(:activity, stem_activity_code: "CP123", programmes: [programme]) } diff --git a/spec/services/cms/models/email_components/text_spec.rb b/spec/services/cms/models/email_components/text_spec.rb index 7d2f484943..3021a4ec35 100644 --- a/spec/services/cms/models/email_components/text_spec.rb +++ b/spec/services/cms/models/email_components/text_spec.rb @@ -10,7 +10,8 @@ email_content: Cms::Mocks::Text::RichBlocks.generate_data, programme_slug: programme.slug, completed_programme_activity_group_slugs: [], - activity_state: :active + activity_state: :active, + enrolled: true ) } diff --git a/spec/support/cms/providers/strapi/strapi_stubs.rb b/spec/support/cms/providers/strapi/strapi_stubs.rb index c971a3f332..5959658c85 100644 --- a/spec/support/cms/providers/strapi/strapi_stubs.rb +++ b/spec/support/cms/providers/strapi/strapi_stubs.rb @@ -189,6 +189,14 @@ def stub_strapi_web_page_not_found(key) end end + def stub_strapi_email_templates(email_templates: Array.new(4) { Cms::Mocks::Collections::EmailTemplate.generate_raw_data }, page: 1, page_size: 10) + if as_graphql + stub_strapi_graphql_collection_query("emailTemplates", email_templates) + else + stub_request(:get, /^https:\/\/strapi.teachcomputing.org\/api\/email-templates/).to_return_json(body: to_strapi_collection(email_templates, page:, page_size:)) + end + end + def stub_strapi_email_template(key, email_template: Cms::Mocks::Collections::EmailTemplate.generate_raw_data) if as_graphql stub_strapi_graphql_query("emailTemplates", email_template, unique_key: key) @@ -197,6 +205,14 @@ def stub_strapi_email_template(key, email_template: Cms::Mocks::Collections::Ema end end + def stub_strapi_email_template_missing(key) + if as_graphql + stub_strapi_graphql_query_missing("emailTemplates") + else + stub_request(:get, /^https:\/\/strapi.teachcomputing.org\/api\/email-templates\/#{key}/).to_return_json(body: not_found_response, status: 404) + end + end + def stub_strapi_programme(key, programme: Cms::Mocks::Collections::Programme.generate_raw_data) if as_graphql stub_strapi_graphql_query("programmes", programme)