From 5b71237c085df709d2ccd3cad6732b19244dd7cc Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Wed, 6 Nov 2024 11:31:45 +0000 Subject: [PATCH 01/15] Creation of a new email model for email templates which will be moved over to Strapi The template model includes a basic "mail merge" feature New component that is a reduced copy of the cms rich text block that converts the rich blocks to a text based version used in the email template --- .../cms_rich_text_block_text_component.rb | 102 +++++++++ .../cms/rich_text_block_component_spec.rb | 50 +---- ...cms_rich_text_block_text_component_spec.rb | 210 ++++++++++++++++++ 3 files changed, 316 insertions(+), 46 deletions(-) create mode 100644 app/components/cms_rich_text_block_text_component.rb create mode 100644 spec/components/cms_rich_text_block_text_component_spec.rb diff --git a/app/components/cms_rich_text_block_text_component.rb b/app/components/cms_rich_text_block_text_component.rb new file mode 100644 index 0000000000..952f4573c1 --- /dev/null +++ b/app/components/cms_rich_text_block_text_component.rb @@ -0,0 +1,102 @@ +# Due to how ERB interacts with newlines and spaces the markup for any +# SubClasses should not include any indentation and should make use of +# `-` at the end of ERB tags +class CmsRichTextBlockTextComponent < ViewComponent::Base + def build(blocks, **options) + klass = + case blocks + in { type: "paragraph" } then Paragraph + in { type: "heading" } then Heading + in { type: "text" } then Text + in { type: "link" } then Link + in { type: "list" } then List + in { type: "list-item" } then ListItem + in { type: "quote"} then Quote + end + + klass.new(blocks: blocks, **options) + end + + erb_template <<~ERB + <% @blocks.each do |child| -%> + <%= render build(child) %> \n + <% end -%> + ERB + + def initialize(blocks:, **options) + @blocks = blocks + @options = options + end + + class Paragraph < CmsRichTextBlockTextComponent + erb_template <<~ERB + <% @blocks[:children].each do |child| -%> + <%= render build(child) -%> + <% end -%> + ERB + end + + class Heading < CmsRichTextBlockTextComponent + erb_template <<~ERB + <% @blocks[:children].each do |child| -%> + <%= render build(child) -%> + <% end -%> + ERB + end + + class Text < CmsRichTextBlockTextComponent + erb_template <<~ERB + <%= @blocks[:text] -%> + ERB + end + + class Link < CmsRichTextBlockTextComponent + # Had to removed indentation in this erb as it was adding whitespace to page render + erb_template <<~ERB + <% @blocks[:children].each do |child| -%> + <%= render build(child) -%> + <% end -%> + <%= url -%> + ERB + + def url + " (#{@blocks[:url]})" + end + end + + class List < CmsRichTextBlockTextComponent + erb_template <<~ERB + <% @blocks[:children].each_with_index do |child, index| -%> + <%= render build(child, type:, index:) -%> + <% end -%> + ERB + + def type + @blocks[:format] + end + end + + class ListItem < CmsRichTextBlockTextComponent + erb_template <<~ERB + <% @blocks[:children].each do |child| -%> + <%= icon -%> <%= render build(child) %> + <% end -%> + ERB + + def icon + if @options[:type] == "ordered" + "#{@options[:index] + 1}." + else + "*" + end + end + end + + class Quote < CmsRichTextBlockTextComponent + erb_template <<~ERB + <% @blocks[:children].each do |child| -%> + <%= render build(child) -%> + <% end -%> + ERB + end +end diff --git a/spec/components/cms/rich_text_block_component_spec.rb b/spec/components/cms/rich_text_block_component_spec.rb index 74dd817ecf..a86e355f93 100644 --- a/spec/components/cms/rich_text_block_component_spec.rb +++ b/spec/components/cms/rich_text_block_component_spec.rb @@ -31,7 +31,7 @@ ] ])) - expect(page).to have_css("p", text: "Hello world!") + expect(page).to have_text("Hello world!") end it "renders a large heading" do @@ -90,46 +90,6 @@ expect(page).to have_text("Just text") end - it "renders bold text" do - render_inline(described_class.new(blocks: [ - {type: "text", text: "Bold text", bold: true} - ])) - - expect(page).to have_css(".cms-rich-text-block-component__text--bold", text: "Bold text") - end - - it "renders italic text" do - render_inline(described_class.new(blocks: [ - {type: "text", text: "Italic text", italic: true} - ])) - - expect(page).to have_css(".cms-rich-text-block-component__text--italic", text: "Italic text") - end - - it "renders underlined text" do - render_inline(described_class.new(blocks: [ - {type: "text", text: "Underlined text", underline: true} - ])) - - expect(page).to have_css(".cms-rich-text-block-component__text--underline", text: "Underlined text") - end - - it "renders strikethrough text" do - render_inline(described_class.new(blocks: [ - {type: "text", text: "Strikethrough text", strikethrough: true} - ])) - - expect(page).to have_css(".cms-rich-text-block-component__text--strikethrough", text: "Strikethrough text") - end - - it "renders code text" do - render_inline(described_class.new(blocks: [ - {type: "text", text: "Code text", code: true} - ])) - - expect(page).to have_css(".cms-rich-text-block-component__text--code", text: "Code text") - end - it "renders a link" do render_inline(described_class.new(blocks: [ { @@ -141,7 +101,7 @@ } ])) - expect(page).to have_link("A link to google", href: "https://www.google.com") + expect(page).to have_text("A link to google (https://www.google.com)") end it "renders an ordered list" do @@ -156,10 +116,8 @@ } ])) - expect(page).to have_css("ol.govuk-list--number") - expect(page).to have_css("ol", count: 1) - expect(page).to have_css("ol li", text: "Item 1") - expect(page).to have_css("ol li", text: "Item 2") + expect(page).to have_text("1. Item 1") + expect(page).to have_text("2. Item 2") end it "renders an unordered list" do diff --git a/spec/components/cms_rich_text_block_text_component_spec.rb b/spec/components/cms_rich_text_block_text_component_spec.rb new file mode 100644 index 0000000000..360b6bd442 --- /dev/null +++ b/spec/components/cms_rich_text_block_text_component_spec.rb @@ -0,0 +1,210 @@ +require "rails_helper" + +RSpec.describe CmsRichTextBlockComponent, type: :component do + it "renders wrapper by default" do + render_inline(described_class.new(blocks: [ + type: "paragraph", + children: [ + {type: "text", text: "Hello world!"} + ] + ])) + + expect(page).to have_css(".govuk-width-container") + end + + it "doesnt render wrapper by turned off" do + render_inline(described_class.new(blocks: [ + type: "paragraph", + children: [ + {type: "text", text: "Hello world!"} + ] + ], with_wrapper: false)) + + expect(page).not_to have_css(".govuk-width-container") + end + + it "renders a paragraph" do + render_inline(described_class.new(blocks: [ + type: "paragraph", + children: [ + {type: "text", text: "Hello world!"} + ] + ])) + + expect(page).to have_css("p", text: "Hello world!") + end + + it "renders a large heading" do + render_inline(described_class.new(blocks: [ + type: "heading", + level: 1, + children: [ + {type: "text", text: "Heading world!"} + ] + ])) + + expect(page).to have_css(".govuk-heading-l", text: "Heading world!") + end + + it "renders a medium heading" do + render_inline(described_class.new(blocks: [ + type: "heading", + level: 2, + children: [ + {type: "text", text: "Heading world!"} + ] + ])) + + expect(page).to have_css(".govuk-heading-m", text: "Heading world!") + end + + it "renders a small heading" do + render_inline(described_class.new(blocks: [ + type: "heading", + level: 3, + children: [ + {type: "text", text: "Heading world!"} + ] + ])) + + expect(page).to have_css(".govuk-heading-s", text: "Heading world!") + end + + it "renders some text" do + render_inline(described_class.new(blocks: [ + {type: "text", text: "Just text"} + ])) + + expect(page).to have_text("Just text") + end + + it "renders bold text" do + render_inline(described_class.new(blocks: [ + {type: "text", text: "Bold text", bold: true} + ])) + + expect(page).to have_css(".cms-rich-text-block-component__text--bold", text: "Bold text") + end + + it "renders italic text" do + render_inline(described_class.new(blocks: [ + {type: "text", text: "Italic text", italic: true} + ])) + + expect(page).to have_css(".cms-rich-text-block-component__text--italic", text: "Italic text") + end + + it "renders underlined text" do + render_inline(described_class.new(blocks: [ + {type: "text", text: "Underlined text", underline: true} + ])) + + expect(page).to have_css(".cms-rich-text-block-component__text--underline", text: "Underlined text") + end + + it "renders strikethrough text" do + render_inline(described_class.new(blocks: [ + {type: "text", text: "Strikethrough text", strikethrough: true} + ])) + + expect(page).to have_css(".cms-rich-text-block-component__text--strikethrough", text: "Strikethrough text") + end + + it "renders code text" do + render_inline(described_class.new(blocks: [ + {type: "text", text: "Code text", code: true} + ])) + + expect(page).to have_css(".cms-rich-text-block-component__text--code", text: "Code text") + end + + it "renders a link" do + render_inline(described_class.new(blocks: [ + { + type: "link", + url: "https://www.google.com", + children: [ + {type: "text", text: "A link to google"} + ] + } + ])) + + expect(page).to have_link("A link to google", href: "https://www.google.com") + end + + it "renders an ordered list" do + render_inline(described_class.new(blocks: [ + { + type: "list", + format: "ordered", + children: [ + {type: "list-item", children: [{type: "text", text: "Item 1"}]}, + {type: "list-item", children: [{type: "text", text: "Item 2"}]} + ] + } + ])) + + expect(page).to have_css("ol.govuk-list--number") + expect(page).to have_css("ol", count: 1) + expect(page).to have_css("ol li", text: "Item 1") + expect(page).to have_css("ol li", text: "Item 2") + end + + it "renders an unordered list" do + render_inline(described_class.new(blocks: [ + { + type: "list", + format: "unordered", + children: [ + {type: "list-item", children: [{type: "text", text: "Item 1"}]}, + {type: "list-item", children: [{type: "text", text: "Item 2"}]} + ] + } + ])) + + expect(page).to have_css("ul", count: 1) + expect(page).to have_css("ul li", text: "Item 1") + expect(page).to have_css("ul li", text: "Item 2") + end + + it "renders an image" do + formats = { + medium: {url: "/an-image-medium.png"}, + large: {url: "/an-image-large.png"} + } + render_inline(described_class.new(blocks: [ + { + type: "image", + image: Cms::Models::Image.new(url: "/an-image.png", alt: "", caption: "", formats: formats, default_size: :medium) + } + ])) + + expect(page).to have_css("img[src='/an-image-medium.png']") + end + + it "renders a quote" do + render_inline(described_class.new(blocks: [ + { + type: "quote", + children: [ + {type: "text", text: "Quoted"} + ] + } + ])) + + expect(page).to have_css("blockquote", text: "Quoted") + end + + it "renders a hr when given three consecutive hyphens" do + render_inline(described_class.new(blocks: [ + { + type: "paragraph", + children: [ + {type: "text", text: "---"} + ] + } + ])) + + expect(page).to have_css("hr") + end +end From 51c559d03e8e185295a661f2db0e00714a1da613 Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Thu, 7 Nov 2024 13:33:32 +0000 Subject: [PATCH 02/15] Moved email content to use a dynamic zone, and creating new email specific models to handle the fact we need to render text and html Setting up methods to get the latest CPD completed, and creating mail merge for that. Setting up new course lists components. Still need to sort out the logic for not showing sections that contain completed courses --- .../cms_email_course_list_component.rb | 31 ++++ .../factories/email_component_factory.rb | 36 +++++ config/environments/development.rb | 3 + .../cms_email_course_list_component_spec.rb | 15 ++ ...cms_rich_text_block_text_component_spec.rb | 135 ++---------------- 5 files changed, 94 insertions(+), 126 deletions(-) create mode 100644 app/components/cms_email_course_list_component.rb create mode 100644 app/services/cms/providers/strapi/factories/email_component_factory.rb create mode 100644 spec/components/cms_email_course_list_component_spec.rb diff --git a/app/components/cms_email_course_list_component.rb b/app/components/cms_email_course_list_component.rb new file mode 100644 index 0000000000..497d05f2ce --- /dev/null +++ b/app/components/cms_email_course_list_component.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class CmsEmailCourseListComponent < ViewComponent::Base + erb_template <<~ERB + + <% if @section_title %> + + <% end %> + <% @course_list.each do |course| %> + + + + <% end %> +

<%= @section_title %>

+ <%= link_to display_name(course), course_link(course) %> +
+ ERB + + def initialize(course_list:, section_title:) + @course_list = course_list + @section_title = section_title + end + + def display_name(course) + course.display_name.presence || course.activity.title + end + + def course_link(course) + course_url(id: course.activity.stem_activity_code, name: course.activity.title.parameterize) + end +end diff --git a/app/services/cms/providers/strapi/factories/email_component_factory.rb b/app/services/cms/providers/strapi/factories/email_component_factory.rb new file mode 100644 index 0000000000..a5512a8d89 --- /dev/null +++ b/app/services/cms/providers/strapi/factories/email_component_factory.rb @@ -0,0 +1,36 @@ +module Cms + module Providers + module Strapi + module Factories + module EmailComponentFactory + def self.process_component(strapi_data) + component_name = strapi_data[:__component] + case component_name + when "email-content.text" + EmailComponents::Text.new( + blocks: strapi_data[:textContent] + ) + when "email-content.cta" + EmailComponents::Cta.new( + text: strapi_data[:text], + link: strapi_data[:link] + ) + when "email-content.course-list" + EmailComponents::CourseList.new( + section_title: strapi_data[:sectionTitle], + remove_on_match: strapi_data[:removeOnMatch], + courses: strapi_data[:courses].map do |course| + EmailComponents::Course.new( + activity_code: course[:activityCode], + substitute: strapi_data[:substitute], + display_name: strapi_data[:displayName] + ) + end + ) + end + end + end + end + end + end +end 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/spec/components/cms_email_course_list_component_spec.rb b/spec/components/cms_email_course_list_component_spec.rb new file mode 100644 index 0000000000..0894847458 --- /dev/null +++ b/spec/components/cms_email_course_list_component_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe CmsEmailCourseListComponent, type: :component do + pending "add some examples to (or delete) #{__FILE__}" + + # it "renders something useful" do + # expect( + # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html + # ).to include( + # "Hello, components!" + # ) + # end +end diff --git a/spec/components/cms_rich_text_block_text_component_spec.rb b/spec/components/cms_rich_text_block_text_component_spec.rb index 360b6bd442..53e35b6c94 100644 --- a/spec/components/cms_rich_text_block_text_component_spec.rb +++ b/spec/components/cms_rich_text_block_text_component_spec.rb @@ -1,28 +1,6 @@ require "rails_helper" -RSpec.describe CmsRichTextBlockComponent, type: :component do - it "renders wrapper by default" do - render_inline(described_class.new(blocks: [ - type: "paragraph", - children: [ - {type: "text", text: "Hello world!"} - ] - ])) - - expect(page).to have_css(".govuk-width-container") - end - - it "doesnt render wrapper by turned off" do - render_inline(described_class.new(blocks: [ - type: "paragraph", - children: [ - {type: "text", text: "Hello world!"} - ] - ], with_wrapper: false)) - - expect(page).not_to have_css(".govuk-width-container") - end - +RSpec.describe CmsRichTextBlockTextComponent, type: :component do it "renders a paragraph" do render_inline(described_class.new(blocks: [ type: "paragraph", @@ -31,7 +9,7 @@ ] ])) - expect(page).to have_css("p", text: "Hello world!") + expect(page).to have_text("Hello world!") end it "renders a large heading" do @@ -43,31 +21,7 @@ ] ])) - expect(page).to have_css(".govuk-heading-l", text: "Heading world!") - end - - it "renders a medium heading" do - render_inline(described_class.new(blocks: [ - type: "heading", - level: 2, - children: [ - {type: "text", text: "Heading world!"} - ] - ])) - - expect(page).to have_css(".govuk-heading-m", text: "Heading world!") - end - - it "renders a small heading" do - render_inline(described_class.new(blocks: [ - type: "heading", - level: 3, - children: [ - {type: "text", text: "Heading world!"} - ] - ])) - - expect(page).to have_css(".govuk-heading-s", text: "Heading world!") + expect(page).to have_text("Heading world!") end it "renders some text" do @@ -78,46 +32,6 @@ expect(page).to have_text("Just text") end - it "renders bold text" do - render_inline(described_class.new(blocks: [ - {type: "text", text: "Bold text", bold: true} - ])) - - expect(page).to have_css(".cms-rich-text-block-component__text--bold", text: "Bold text") - end - - it "renders italic text" do - render_inline(described_class.new(blocks: [ - {type: "text", text: "Italic text", italic: true} - ])) - - expect(page).to have_css(".cms-rich-text-block-component__text--italic", text: "Italic text") - end - - it "renders underlined text" do - render_inline(described_class.new(blocks: [ - {type: "text", text: "Underlined text", underline: true} - ])) - - expect(page).to have_css(".cms-rich-text-block-component__text--underline", text: "Underlined text") - end - - it "renders strikethrough text" do - render_inline(described_class.new(blocks: [ - {type: "text", text: "Strikethrough text", strikethrough: true} - ])) - - expect(page).to have_css(".cms-rich-text-block-component__text--strikethrough", text: "Strikethrough text") - end - - it "renders code text" do - render_inline(described_class.new(blocks: [ - {type: "text", text: "Code text", code: true} - ])) - - expect(page).to have_css(".cms-rich-text-block-component__text--code", text: "Code text") - end - it "renders a link" do render_inline(described_class.new(blocks: [ { @@ -129,7 +43,7 @@ } ])) - expect(page).to have_link("A link to google", href: "https://www.google.com") + expect(page).to have_text("A link to google (https://www.google.com)") end it "renders an ordered list" do @@ -144,10 +58,8 @@ } ])) - expect(page).to have_css("ol.govuk-list--number") - expect(page).to have_css("ol", count: 1) - expect(page).to have_css("ol li", text: "Item 1") - expect(page).to have_css("ol li", text: "Item 2") + expect(page).to have_text("1. Item 1") + expect(page).to have_text("2. Item 2") end it "renders an unordered list" do @@ -162,24 +74,8 @@ } ])) - expect(page).to have_css("ul", count: 1) - expect(page).to have_css("ul li", text: "Item 1") - expect(page).to have_css("ul li", text: "Item 2") - end - - it "renders an image" do - formats = { - medium: {url: "/an-image-medium.png"}, - large: {url: "/an-image-large.png"} - } - render_inline(described_class.new(blocks: [ - { - type: "image", - image: Cms::Models::Image.new(url: "/an-image.png", alt: "", caption: "", formats: formats, default_size: :medium) - } - ])) - - expect(page).to have_css("img[src='/an-image-medium.png']") + expect(page).to have_text("* Item 1") + expect(page).to have_text("* Item 2") end it "renders a quote" do @@ -192,19 +88,6 @@ } ])) - expect(page).to have_css("blockquote", text: "Quoted") - end - - it "renders a hr when given three consecutive hyphens" do - render_inline(described_class.new(blocks: [ - { - type: "paragraph", - children: [ - {type: "text", text: "---"} - ] - } - ])) - - expect(page).to have_css("hr") + expect(page).to have_text("Quoted") end end From 5a5bbb7216a410190e24eb27354269897348a96d Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Wed, 18 Dec 2024 12:23:08 +0000 Subject: [PATCH 03/15] Sorting schema --- erd.pdf | Bin 51077 -> 51077 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/erd.pdf b/erd.pdf index dc830cedf55f54fb38b7c23be9fca7ceffe37d09..353144ed8e26bcde79d3d4414649c4e8d9ec0125 100644 GIT binary patch delta 288 zcmV+*0pI?Gj{}8|1E9r!PiurQ5XJ9)igzjKMU$xgv&bG+VJW2`Zci;egc#gFF_Og6 z{q{}V)lzbrAM@tDnNX0ZU?@q%K#TjR$dNzLsH%{?T_%aF<%6^lfb2!}79U_B=}*#msqbf4@?e8Ae2%FNlz&~2t_!8Vi`#; z&A+c?$0{K); zb}LT#j0!fVOSz!(JQ?TzCBjp3&C-MNCP&Djdhlj0R&Cr*N=nn?OECizf;|#9F$n_X zL$UX+Yf)9X$o1gT(-EB#^{NSNcGI^JzaaX~jMX-E?aa5;=D6Qvm{MncG{JRVn*j0f mJSCxyL->%s;NOB0TNTxptMB`z^WWD?O}|%3ZiTZD#&rR7pNvQV From b3f7e8d53f76d6cfd3a1064ec5e49f290f24fa83 Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Fri, 3 Jan 2025 13:14:00 +0000 Subject: [PATCH 04/15] Adding sent emails to administrate to allow us to check email sending easier --- .../admin/sent_emails_controller.rb | 46 ++++++++++++++++ app/dashboards/sent_email_dashboard.rb | 54 +++++++++++++++++++ app/dashboards/user_dashboard.rb | 4 +- app/jobs/send_cms_emails_job.rb | 9 ++++ config/routes.rb | 1 + spec/requests/admin/sent_emails_spec.rb | 29 ++++++++++ 6 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 app/controllers/admin/sent_emails_controller.rb create mode 100644 app/dashboards/sent_email_dashboard.rb create mode 100644 app/jobs/send_cms_emails_job.rb create mode 100644 spec/requests/admin/sent_emails_spec.rb 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..0bc2da4e7a --- /dev/null +++ b/app/jobs/send_cms_emails_job.rb @@ -0,0 +1,9 @@ +class SendInactivityEmailsJob < ApplicationJob + def perform + # Get all published email templates + + # Do Activity query + + # Send email + end +end 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/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 From 7d8212ac2544346d16466f6582954d3cff3b2ca3 Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Wed, 8 Jan 2025 16:59:59 +0000 Subject: [PATCH 05/15] Initial setup of send cms email job. Tweak to the strapi stub system to better handle pagination in tests Adding support fot slug access in emailtemplate all resouce queries --- app/jobs/send_cms_emails_job.rb | 21 +++++++++++++++++---- spec/jobs/send_cms_emails_job_spec.rb | 23 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 spec/jobs/send_cms_emails_job_spec.rb diff --git a/app/jobs/send_cms_emails_job.rb b/app/jobs/send_cms_emails_job.rb index 0bc2da4e7a..9d0020deca 100644 --- a/app/jobs/send_cms_emails_job.rb +++ b/app/jobs/send_cms_emails_job.rb @@ -1,9 +1,22 @@ -class SendInactivityEmailsJob < ApplicationJob +class SendCmsEmailsJob < ApplicationJob + PER_PAGE = 50 def perform - # Get all published email templates + page = 1 + loop do + email_templates = Cms::Collections::EmailTemplate.all(page, PER_PAGE) + break if email_templates.resources.empty? - # Do Activity query + email_templates.resources.each { process_template(_1) } + page += 1 + end + end + + def process_template(template) + # Do Query + users = [] - # Send email + users.each do |user| + CmsMailer.with(template_slug: template.template.slug, user_id: user.id).send_template.deliver_later + end end end 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..7c2e9b88a2 --- /dev/null +++ b/spec/jobs/send_cms_emails_job_spec.rb @@ -0,0 +1,23 @@ +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(:email_template_1) { + Cms::Mocks::EmailTemplate.generate_raw_data(slug: email_template_1_slug) + } + let(:email_template_2) { + Cms::Mocks::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]) + end + + it "should iterate through templates" do + described_class.perform_now + end +end From 78192b52fa84eb9229d22083fe6f034ef04d3482 Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Wed, 5 Feb 2025 10:00:10 +0000 Subject: [PATCH 06/15] Adding missing enrolled parameter --- app/jobs/send_cms_emails_job.rb | 5 ++- app/mailers/cms_mailer.rb | 3 +- .../cms/models/collections/email_template.rb | 5 ++- .../strapi/factories/model_factory.rb | 3 +- .../mocks/collections/email_template.rb | 1 + spec/mailers/cms_mailer_spec.rb | 39 +++++++++---------- .../models/collections/email_template_spec.rb | 3 +- .../email_components/base_component_spec.rb | 3 +- .../email_components/course_list_spec.rb | 3 +- .../cms/models/email_components/text_spec.rb | 3 +- 10 files changed, 37 insertions(+), 31 deletions(-) diff --git a/app/jobs/send_cms_emails_job.rb b/app/jobs/send_cms_emails_job.rb index 9d0020deca..39351e69db 100644 --- a/app/jobs/send_cms_emails_job.rb +++ b/app/jobs/send_cms_emails_job.rb @@ -13,10 +13,11 @@ def perform def process_template(template) # Do Query - users = [] + 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: template.template.slug, user_id: user.id).send_template.deliver_later + CmsMailer.with(template: data, 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..8fe558d6f6 100644 --- a/app/mailers/cms_mailer.rb +++ b/app/mailers/cms_mailer.rb @@ -1,8 +1,7 @@ class CmsMailer < ApplicationMailer def send_template - template_slug = params[:template_slug] + @template = params[:template] @user = User.find(params[:user_id]) - @template = Cms::Collections::EmailTemplate.get(template_slug).template @subject = @template.subject(@user) 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/spec/mailers/cms_mailer_spec.rb b/spec/mailers/cms_mailer_spec.rb index 11bff0e5ff..f436016325 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}"} ] }, { @@ -51,28 +51,27 @@ ] } let(:email_template) { - Cms::Mocks::Collections::EmailTemplate.generate_raw_data( - subject:, - slug:, - email_content: + Cms::Providers::Strapi::Factories::ModelFactory.to_email_template( + Cms::Mocks::Collections::EmailTemplate.generate_data( + subject:, + slug:, + email_content: + ) ) } let(:email_template_merge_subject) { - Cms::Mocks::Collections::EmailTemplate.generate_raw_data( - subject: "Congrats {first_name} you did {last_cpd_title}", - slug: slug_with_merge_subject, - email_content: + Cms::Providers::Strapi::Factories::ModelFactory.to_email_template( + Cms::Mocks::Collections::EmailTemplate.generate_data( + subject: "Congrats {first_name} you did {last_cpd_title}", + slug: slug_with_merge_subject, + email_content: + ) ) } - before do - stub_strapi_email_template(slug, email_template:) - stub_strapi_email_template(slug_with_merge_subject, email_template: email_template_merge_subject) - end - describe "send_template" do before do - @mail = CmsMailer.with(user_id: user.id, template_slug: slug).send_template + @mail = CmsMailer.with(user_id: user.id, template: email_template).send_template end it "renders the headers" do @@ -131,7 +130,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 = CmsMailer.with(user_id: user.id, template: email_template).send_template end end @@ -147,7 +146,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 = CmsMailer.with(user_id: user.id, template: email_template_merge_subject).send_template end it "renders the headers" do diff --git a/spec/services/cms/models/collections/email_template_spec.rb b/spec/services/cms/models/collections/email_template_spec.rb index 5aeb76ee11..e8b7827d2b 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 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 ) } From c2948cbe90744d4c84607c992440f2e25a7ba06d Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Wed, 5 Feb 2025 16:57:14 +0000 Subject: [PATCH 07/15] Improving testing and putting back yesterdays stupid changes --- app/jobs/send_cms_emails_job.rb | 2 +- app/mailers/cms_mailer.rb | 12 +++-- spec/jobs/send_cms_emails_job_spec.rb | 12 ++++- spec/mailers/cms_mailer_spec.rb | 47 +++++++++++++------ .../cms/providers/strapi/strapi_stubs.rb | 8 ++++ 5 files changed, 60 insertions(+), 21 deletions(-) diff --git a/app/jobs/send_cms_emails_job.rb b/app/jobs/send_cms_emails_job.rb index 39351e69db..f879a71429 100644 --- a/app/jobs/send_cms_emails_job.rb +++ b/app/jobs/send_cms_emails_job.rb @@ -17,7 +17,7 @@ def process_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: data, user_id: user.id).send_template.deliver_later + 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 8fe558d6f6..ac68acd86b 100644 --- a/app/mailers/cms_mailer.rb +++ b/app/mailers/cms_mailer.rb @@ -1,10 +1,16 @@ class CmsMailer < ApplicationMailer def send_template - @template = params[:template] + @template_slug = params[:template_slug] @user = User.find(params[:user_id]) - @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/spec/jobs/send_cms_emails_job_spec.rb b/spec/jobs/send_cms_emails_job_spec.rb index 7c2e9b88a2..15e20443f5 100644 --- a/spec/jobs/send_cms_emails_job_spec.rb +++ b/spec/jobs/send_cms_emails_job_spec.rb @@ -3,6 +3,7 @@ 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::EmailTemplate.generate_raw_data(slug: email_template_1_slug) @@ -15,9 +16,16 @@ 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]) + + allow_any_instance_of(Programmes::ProgressQuery).to receive(:call).and_return(users) end - it "should iterate through templates" do - described_class.perform_now + 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 f436016325..7c094c357c 100644 --- a/spec/mailers/cms_mailer_spec.rb +++ b/spec/mailers/cms_mailer_spec.rb @@ -51,27 +51,28 @@ ] } let(:email_template) { - Cms::Providers::Strapi::Factories::ModelFactory.to_email_template( - Cms::Mocks::Collections::EmailTemplate.generate_data( - subject:, - slug:, - email_content: - ) + Cms::Mocks::Collections::EmailTemplate.generate_raw_data( + subject:, + slug:, + email_content: ) } let(:email_template_merge_subject) { - Cms::Providers::Strapi::Factories::ModelFactory.to_email_template( - Cms::Mocks::Collections::EmailTemplate.generate_data( - subject: "Congrats {first_name} you did {last_cpd_title}", - slug: slug_with_merge_subject, - email_content: - ) + Cms::Mocks::Collections::EmailTemplate.generate_raw_data( + subject: "Congrats {first_name} you did {last_cpd_title}", + slug: slug_with_merge_subject, + email_content: ) } + before do + stub_strapi_email_template(slug, email_template:) + stub_strapi_email_template(slug_with_merge_subject, email_template: email_template_merge_subject) + end + describe "send_template" do before do - @mail = CmsMailer.with(user_id: user.id, template: email_template).send_template + @mail = described_class.with(user_id: user.id, template_slug: slug).send_template end it "renders the headers" do @@ -130,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: email_template).send_template + @future_mail = described_class.with(user_id: user.id, template_slug: slug).send_template end end @@ -146,7 +147,7 @@ describe "send_template with merged subject" do before do - @mail = CmsMailer.with(user_id: user.id, template: email_template_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 @@ -155,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/support/cms/providers/strapi/strapi_stubs.rb b/spec/support/cms/providers/strapi/strapi_stubs.rb index c971a3f332..987bc359e6 100644 --- a/spec/support/cms/providers/strapi/strapi_stubs.rb +++ b/spec/support/cms/providers/strapi/strapi_stubs.rb @@ -197,6 +197,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) From 666640638f1dd0c1ff2b8fd937ea41fe381720a2 Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Thu, 20 Feb 2025 09:43:19 +0000 Subject: [PATCH 08/15] Sort out issues after rebasing --- app/jobs/send_cms_emails_job.rb | 3 +- .../cms/collections/email_template.rb | 2 +- .../cms/rich_text_block_component_spec.rb | 50 +++++++++- .../rich_text_block_text_component_spec.rb | 2 +- .../cms_email_course_list_component_spec.rb | 15 --- ...cms_rich_text_block_text_component_spec.rb | 93 ------------------- spec/jobs/send_cms_emails_job_spec.rb | 2 +- .../cms/providers/strapi/strapi_stubs.rb | 8 ++ 8 files changed, 58 insertions(+), 117 deletions(-) delete mode 100644 spec/components/cms_email_course_list_component_spec.rb delete mode 100644 spec/components/cms_rich_text_block_text_component_spec.rb diff --git a/app/jobs/send_cms_emails_job.rb b/app/jobs/send_cms_emails_job.rb index f879a71429..5c79aaab37 100644 --- a/app/jobs/send_cms_emails_job.rb +++ b/app/jobs/send_cms_emails_job.rb @@ -4,9 +4,8 @@ def perform page = 1 loop do email_templates = Cms::Collections::EmailTemplate.all(page, PER_PAGE) - break if email_templates.resources.empty? - email_templates.resources.each { process_template(_1) } + break if (page * PER_PAGE) > email_templates.total_records page += 1 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/spec/components/cms/rich_text_block_component_spec.rb b/spec/components/cms/rich_text_block_component_spec.rb index a86e355f93..74dd817ecf 100644 --- a/spec/components/cms/rich_text_block_component_spec.rb +++ b/spec/components/cms/rich_text_block_component_spec.rb @@ -31,7 +31,7 @@ ] ])) - expect(page).to have_text("Hello world!") + expect(page).to have_css("p", text: "Hello world!") end it "renders a large heading" do @@ -90,6 +90,46 @@ expect(page).to have_text("Just text") end + it "renders bold text" do + render_inline(described_class.new(blocks: [ + {type: "text", text: "Bold text", bold: true} + ])) + + expect(page).to have_css(".cms-rich-text-block-component__text--bold", text: "Bold text") + end + + it "renders italic text" do + render_inline(described_class.new(blocks: [ + {type: "text", text: "Italic text", italic: true} + ])) + + expect(page).to have_css(".cms-rich-text-block-component__text--italic", text: "Italic text") + end + + it "renders underlined text" do + render_inline(described_class.new(blocks: [ + {type: "text", text: "Underlined text", underline: true} + ])) + + expect(page).to have_css(".cms-rich-text-block-component__text--underline", text: "Underlined text") + end + + it "renders strikethrough text" do + render_inline(described_class.new(blocks: [ + {type: "text", text: "Strikethrough text", strikethrough: true} + ])) + + expect(page).to have_css(".cms-rich-text-block-component__text--strikethrough", text: "Strikethrough text") + end + + it "renders code text" do + render_inline(described_class.new(blocks: [ + {type: "text", text: "Code text", code: true} + ])) + + expect(page).to have_css(".cms-rich-text-block-component__text--code", text: "Code text") + end + it "renders a link" do render_inline(described_class.new(blocks: [ { @@ -101,7 +141,7 @@ } ])) - expect(page).to have_text("A link to google (https://www.google.com)") + expect(page).to have_link("A link to google", href: "https://www.google.com") end it "renders an ordered list" do @@ -116,8 +156,10 @@ } ])) - expect(page).to have_text("1. Item 1") - expect(page).to have_text("2. Item 2") + expect(page).to have_css("ol.govuk-list--number") + expect(page).to have_css("ol", count: 1) + expect(page).to have_css("ol li", text: "Item 1") + expect(page).to have_css("ol li", text: "Item 2") end it "renders an unordered list" do diff --git a/spec/components/cms/rich_text_block_text_component_spec.rb b/spec/components/cms/rich_text_block_text_component_spec.rb index d213225770..53e35b6c94 100644 --- a/spec/components/cms/rich_text_block_text_component_spec.rb +++ b/spec/components/cms/rich_text_block_text_component_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe Cms::RichTextBlockTextComponent, type: :component do +RSpec.describe CmsRichTextBlockTextComponent, type: :component do it "renders a paragraph" do render_inline(described_class.new(blocks: [ type: "paragraph", diff --git a/spec/components/cms_email_course_list_component_spec.rb b/spec/components/cms_email_course_list_component_spec.rb deleted file mode 100644 index 0894847458..0000000000 --- a/spec/components/cms_email_course_list_component_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require "rails_helper" - -RSpec.describe CmsEmailCourseListComponent, type: :component do - pending "add some examples to (or delete) #{__FILE__}" - - # it "renders something useful" do - # expect( - # render_inline(described_class.new(attr: "value")) { "Hello, components!" }.css("p").to_html - # ).to include( - # "Hello, components!" - # ) - # end -end diff --git a/spec/components/cms_rich_text_block_text_component_spec.rb b/spec/components/cms_rich_text_block_text_component_spec.rb deleted file mode 100644 index 53e35b6c94..0000000000 --- a/spec/components/cms_rich_text_block_text_component_spec.rb +++ /dev/null @@ -1,93 +0,0 @@ -require "rails_helper" - -RSpec.describe CmsRichTextBlockTextComponent, type: :component do - it "renders a paragraph" do - render_inline(described_class.new(blocks: [ - type: "paragraph", - children: [ - {type: "text", text: "Hello world!"} - ] - ])) - - expect(page).to have_text("Hello world!") - end - - it "renders a large heading" do - render_inline(described_class.new(blocks: [ - type: "heading", - level: 1, - children: [ - {type: "text", text: "Heading world!"} - ] - ])) - - expect(page).to have_text("Heading world!") - end - - it "renders some text" do - render_inline(described_class.new(blocks: [ - {type: "text", text: "Just text"} - ])) - - expect(page).to have_text("Just text") - end - - it "renders a link" do - render_inline(described_class.new(blocks: [ - { - type: "link", - url: "https://www.google.com", - children: [ - {type: "text", text: "A link to google"} - ] - } - ])) - - expect(page).to have_text("A link to google (https://www.google.com)") - end - - it "renders an ordered list" do - render_inline(described_class.new(blocks: [ - { - type: "list", - format: "ordered", - children: [ - {type: "list-item", children: [{type: "text", text: "Item 1"}]}, - {type: "list-item", children: [{type: "text", text: "Item 2"}]} - ] - } - ])) - - expect(page).to have_text("1. Item 1") - expect(page).to have_text("2. Item 2") - end - - it "renders an unordered list" do - render_inline(described_class.new(blocks: [ - { - type: "list", - format: "unordered", - children: [ - {type: "list-item", children: [{type: "text", text: "Item 1"}]}, - {type: "list-item", children: [{type: "text", text: "Item 2"}]} - ] - } - ])) - - expect(page).to have_text("* Item 1") - expect(page).to have_text("* Item 2") - end - - it "renders a quote" do - render_inline(described_class.new(blocks: [ - { - type: "quote", - children: [ - {type: "text", text: "Quoted"} - ] - } - ])) - - expect(page).to have_text("Quoted") - end -end diff --git a/spec/jobs/send_cms_emails_job_spec.rb b/spec/jobs/send_cms_emails_job_spec.rb index 15e20443f5..dba416a0b8 100644 --- a/spec/jobs/send_cms_emails_job_spec.rb +++ b/spec/jobs/send_cms_emails_job_spec.rb @@ -15,7 +15,7 @@ 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]) + 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 diff --git a/spec/support/cms/providers/strapi/strapi_stubs.rb b/spec/support/cms/providers/strapi/strapi_stubs.rb index 987bc359e6..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) From 7c316365ef97aa2f6f2aa9b90a7265f33e618521 Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Thu, 20 Feb 2025 15:17:29 +0000 Subject: [PATCH 09/15] Fixing standardrb errors --- app/components/cms_rich_text_block_text_component.rb | 4 ++-- spec/jobs/send_cms_emails_job_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/cms_rich_text_block_text_component.rb b/app/components/cms_rich_text_block_text_component.rb index 952f4573c1..69c26be6b6 100644 --- a/app/components/cms_rich_text_block_text_component.rb +++ b/app/components/cms_rich_text_block_text_component.rb @@ -2,7 +2,7 @@ # SubClasses should not include any indentation and should make use of # `-` at the end of ERB tags class CmsRichTextBlockTextComponent < ViewComponent::Base - def build(blocks, **options) + def build(blocks, **) klass = case blocks in { type: "paragraph" } then Paragraph @@ -14,7 +14,7 @@ def build(blocks, **options) in { type: "quote"} then Quote end - klass.new(blocks: blocks, **options) + klass.new(blocks: blocks, **) end erb_template <<~ERB diff --git a/spec/jobs/send_cms_emails_job_spec.rb b/spec/jobs/send_cms_emails_job_spec.rb index dba416a0b8..69212c4464 100644 --- a/spec/jobs/send_cms_emails_job_spec.rb +++ b/spec/jobs/send_cms_emails_job_spec.rb @@ -6,10 +6,10 @@ let(:users) { create_list(:user, 2) } let(:email_template_1) { - Cms::Mocks::EmailTemplate.generate_raw_data(slug: email_template_1_slug) + Cms::Mocks::Collections::EmailTemplate.generate_raw_data(slug: email_template_1_slug) } let(:email_template_2) { - Cms::Mocks::EmailTemplate.generate_raw_data(slug: email_template_2_slug) + Cms::Mocks::Collections::EmailTemplate.generate_raw_data(slug: email_template_2_slug) } before do From 736c20ef56aac403b70c39c6ab1a21c127a182ef Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Thu, 20 Feb 2025 15:26:17 +0000 Subject: [PATCH 10/15] Removing duplicated factory --- .../strapi/factories/component_factory.rb | 2 +- .../factories/email_component_factory.rb | 14 ++++---- .../strapi/factories/email_content_factory.rb | 36 ------------------- 3 files changed, 8 insertions(+), 44 deletions(-) delete mode 100644 app/services/cms/providers/strapi/factories/email_content_factory.rb diff --git a/app/services/cms/providers/strapi/factories/component_factory.rb b/app/services/cms/providers/strapi/factories/component_factory.rb index 48bd46b76b..3082ee53e2 100644 --- a/app/services/cms/providers/strapi/factories/component_factory.rb +++ b/app/services/cms/providers/strapi/factories/component_factory.rb @@ -23,7 +23,7 @@ def self.process_component(strapi_data) when "content-blocks" ContentBlockFactory.generate_component(name, strapi_data) when "email-content" - EmailContentFactory.generate_component(name, strapi_data) + EmailComponentFactory.generate_component(name, strapi_data) end end end diff --git a/app/services/cms/providers/strapi/factories/email_component_factory.rb b/app/services/cms/providers/strapi/factories/email_component_factory.rb index a5512a8d89..1eecad5c4e 100644 --- a/app/services/cms/providers/strapi/factories/email_component_factory.rb +++ b/app/services/cms/providers/strapi/factories/email_component_factory.rb @@ -3,27 +3,27 @@ module Providers module Strapi module Factories module EmailComponentFactory - def self.process_component(strapi_data) - component_name = strapi_data[:__component] + include BaseFactory + def self.generate_component(component_name, strapi_data) case component_name - when "email-content.text" + when "text" EmailComponents::Text.new( blocks: strapi_data[:textContent] ) - when "email-content.cta" + when "cta" EmailComponents::Cta.new( text: strapi_data[:text], link: strapi_data[:link] ) - when "email-content.course-list" + when "course-list" EmailComponents::CourseList.new( section_title: strapi_data[:sectionTitle], remove_on_match: strapi_data[:removeOnMatch], courses: strapi_data[:courses].map do |course| EmailComponents::Course.new( activity_code: course[:activityCode], - substitute: strapi_data[:substitute], - display_name: strapi_data[:displayName] + substitute: course[:substitute], + display_name: course[:displayName] ) end ) diff --git a/app/services/cms/providers/strapi/factories/email_content_factory.rb b/app/services/cms/providers/strapi/factories/email_content_factory.rb deleted file mode 100644 index c5a83da539..0000000000 --- a/app/services/cms/providers/strapi/factories/email_content_factory.rb +++ /dev/null @@ -1,36 +0,0 @@ -module Cms - module Providers - module Strapi - module Factories - module EmailContentFactory - include BaseFactory - def self.generate_component(component_name, strapi_data) - case component_name - when "text" - Models::EmailComponents::Text.new( - blocks: strapi_data[:textContent] - ) - when "cta" - Models::EmailComponents::Cta.new( - text: strapi_data[:text], - link: strapi_data[:link] - ) - when "course-list" - Models::EmailComponents::CourseList.new( - section_title: strapi_data[:sectionTitle], - remove_on_match: strapi_data[:removeOnMatch], - courses: strapi_data[:courses].map do |course| - Models::EmailComponents::Course.new( - activity_code: course[:activityCode], - substitute: course[:substitute], - display_name: course[:displayName] - ) - end - ) - end - end - end - end - end - end -end From df13a8bf28df99c3ade320274382651096e02ada Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Thu, 20 Feb 2025 15:58:57 +0000 Subject: [PATCH 11/15] Changing that back --- .../cms/providers/strapi/factories/component_factory.rb | 2 +- .../{email_component_factory.rb => email_content_factory.rb} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/services/cms/providers/strapi/factories/{email_component_factory.rb => email_content_factory.rb} (96%) diff --git a/app/services/cms/providers/strapi/factories/component_factory.rb b/app/services/cms/providers/strapi/factories/component_factory.rb index 3082ee53e2..48bd46b76b 100644 --- a/app/services/cms/providers/strapi/factories/component_factory.rb +++ b/app/services/cms/providers/strapi/factories/component_factory.rb @@ -23,7 +23,7 @@ def self.process_component(strapi_data) when "content-blocks" ContentBlockFactory.generate_component(name, strapi_data) when "email-content" - EmailComponentFactory.generate_component(name, strapi_data) + EmailContentFactory.generate_component(name, strapi_data) end end end diff --git a/app/services/cms/providers/strapi/factories/email_component_factory.rb b/app/services/cms/providers/strapi/factories/email_content_factory.rb similarity index 96% rename from app/services/cms/providers/strapi/factories/email_component_factory.rb rename to app/services/cms/providers/strapi/factories/email_content_factory.rb index 1eecad5c4e..2b0b15363c 100644 --- a/app/services/cms/providers/strapi/factories/email_component_factory.rb +++ b/app/services/cms/providers/strapi/factories/email_content_factory.rb @@ -2,7 +2,7 @@ module Cms module Providers module Strapi module Factories - module EmailComponentFactory + module EmailContentFactory include BaseFactory def self.generate_component(component_name, strapi_data) case component_name From 43d61bf9e5a0a948be64b1702fae76139b9af65e Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Thu, 20 Mar 2025 10:56:32 +0000 Subject: [PATCH 12/15] Removing errant file, and using the all_records method now to simplify sending logic --- .../cms_email_course_list_component.rb | 31 ------------------- app/jobs/send_cms_emails_job.rb | 9 ++---- 2 files changed, 2 insertions(+), 38 deletions(-) delete mode 100644 app/components/cms_email_course_list_component.rb diff --git a/app/components/cms_email_course_list_component.rb b/app/components/cms_email_course_list_component.rb deleted file mode 100644 index 497d05f2ce..0000000000 --- a/app/components/cms_email_course_list_component.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class CmsEmailCourseListComponent < ViewComponent::Base - erb_template <<~ERB - - <% if @section_title %> - - <% end %> - <% @course_list.each do |course| %> - - - - <% end %> -

<%= @section_title %>

- <%= link_to display_name(course), course_link(course) %> -
- ERB - - def initialize(course_list:, section_title:) - @course_list = course_list - @section_title = section_title - end - - def display_name(course) - course.display_name.presence || course.activity.title - end - - def course_link(course) - course_url(id: course.activity.stem_activity_code, name: course.activity.title.parameterize) - end -end diff --git a/app/jobs/send_cms_emails_job.rb b/app/jobs/send_cms_emails_job.rb index 5c79aaab37..fe409403dc 100644 --- a/app/jobs/send_cms_emails_job.rb +++ b/app/jobs/send_cms_emails_job.rb @@ -1,13 +1,8 @@ class SendCmsEmailsJob < ApplicationJob PER_PAGE = 50 def perform - page = 1 - loop do - email_templates = Cms::Collections::EmailTemplate.all(page, PER_PAGE) - email_templates.resources.each { process_template(_1) } - break if (page * PER_PAGE) > email_templates.total_records - page += 1 - end + email_templates = Cms::Collections::EmailTemplate.all_records + email_templates.each { process_template(_1) } end def process_template(template) From 8fc79aa58df77280908ca81cce519b1fda3d03ef Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Thu, 20 Mar 2025 12:59:42 +0000 Subject: [PATCH 13/15] Removing unused component and fixing name reference --- .../cms_rich_text_block_text_component.rb | 102 ------------------ .../rich_text_block_text_component_spec.rb | 2 +- 2 files changed, 1 insertion(+), 103 deletions(-) delete mode 100644 app/components/cms_rich_text_block_text_component.rb diff --git a/app/components/cms_rich_text_block_text_component.rb b/app/components/cms_rich_text_block_text_component.rb deleted file mode 100644 index 69c26be6b6..0000000000 --- a/app/components/cms_rich_text_block_text_component.rb +++ /dev/null @@ -1,102 +0,0 @@ -# Due to how ERB interacts with newlines and spaces the markup for any -# SubClasses should not include any indentation and should make use of -# `-` at the end of ERB tags -class CmsRichTextBlockTextComponent < ViewComponent::Base - def build(blocks, **) - klass = - case blocks - in { type: "paragraph" } then Paragraph - in { type: "heading" } then Heading - in { type: "text" } then Text - in { type: "link" } then Link - in { type: "list" } then List - in { type: "list-item" } then ListItem - in { type: "quote"} then Quote - end - - klass.new(blocks: blocks, **) - end - - erb_template <<~ERB - <% @blocks.each do |child| -%> - <%= render build(child) %> \n - <% end -%> - ERB - - def initialize(blocks:, **options) - @blocks = blocks - @options = options - end - - class Paragraph < CmsRichTextBlockTextComponent - erb_template <<~ERB - <% @blocks[:children].each do |child| -%> - <%= render build(child) -%> - <% end -%> - ERB - end - - class Heading < CmsRichTextBlockTextComponent - erb_template <<~ERB - <% @blocks[:children].each do |child| -%> - <%= render build(child) -%> - <% end -%> - ERB - end - - class Text < CmsRichTextBlockTextComponent - erb_template <<~ERB - <%= @blocks[:text] -%> - ERB - end - - class Link < CmsRichTextBlockTextComponent - # Had to removed indentation in this erb as it was adding whitespace to page render - erb_template <<~ERB - <% @blocks[:children].each do |child| -%> - <%= render build(child) -%> - <% end -%> - <%= url -%> - ERB - - def url - " (#{@blocks[:url]})" - end - end - - class List < CmsRichTextBlockTextComponent - erb_template <<~ERB - <% @blocks[:children].each_with_index do |child, index| -%> - <%= render build(child, type:, index:) -%> - <% end -%> - ERB - - def type - @blocks[:format] - end - end - - class ListItem < CmsRichTextBlockTextComponent - erb_template <<~ERB - <% @blocks[:children].each do |child| -%> - <%= icon -%> <%= render build(child) %> - <% end -%> - ERB - - def icon - if @options[:type] == "ordered" - "#{@options[:index] + 1}." - else - "*" - end - end - end - - class Quote < CmsRichTextBlockTextComponent - erb_template <<~ERB - <% @blocks[:children].each do |child| -%> - <%= render build(child) -%> - <% end -%> - ERB - end -end diff --git a/spec/components/cms/rich_text_block_text_component_spec.rb b/spec/components/cms/rich_text_block_text_component_spec.rb index 53e35b6c94..d213225770 100644 --- a/spec/components/cms/rich_text_block_text_component_spec.rb +++ b/spec/components/cms/rich_text_block_text_component_spec.rb @@ -1,6 +1,6 @@ require "rails_helper" -RSpec.describe CmsRichTextBlockTextComponent, type: :component do +RSpec.describe Cms::RichTextBlockTextComponent, type: :component do it "renders a paragraph" do render_inline(described_class.new(blocks: [ type: "paragraph", From b2b3f6a1c6ae894c38633b00e183c814bfdab3bc Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Thu, 20 Mar 2025 16:29:27 +0000 Subject: [PATCH 14/15] Adding some extra testing --- .../cms/models/collections/email_template_spec.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spec/services/cms/models/collections/email_template_spec.rb b/spec/services/cms/models/collections/email_template_spec.rb index e8b7827d2b..f4e13c3f79 100644 --- a/spec/services/cms/models/collections/email_template_spec.rb +++ b/spec/services/cms/models/collections/email_template_spec.rb @@ -84,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 From 74f16c3ca5917f8426ae42314293c4b87b26191e Mon Sep 17 00:00:00 2001 From: Michael Squance Date: Mon, 14 Apr 2025 12:29:25 +0000 Subject: [PATCH 15/15] Fixing incorrect reference in factory --- .../providers/strapi/factories/email_content_factory.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/cms/providers/strapi/factories/email_content_factory.rb b/app/services/cms/providers/strapi/factories/email_content_factory.rb index 2b0b15363c..c5a83da539 100644 --- a/app/services/cms/providers/strapi/factories/email_content_factory.rb +++ b/app/services/cms/providers/strapi/factories/email_content_factory.rb @@ -7,20 +7,20 @@ module EmailContentFactory def self.generate_component(component_name, strapi_data) case component_name when "text" - EmailComponents::Text.new( + Models::EmailComponents::Text.new( blocks: strapi_data[:textContent] ) when "cta" - EmailComponents::Cta.new( + Models::EmailComponents::Cta.new( text: strapi_data[:text], link: strapi_data[:link] ) when "course-list" - EmailComponents::CourseList.new( + Models::EmailComponents::CourseList.new( section_title: strapi_data[:sectionTitle], remove_on_match: strapi_data[:removeOnMatch], courses: strapi_data[:courses].map do |course| - EmailComponents::Course.new( + Models::EmailComponents::Course.new( activity_code: course[:activityCode], substitute: course[:substitute], display_name: course[:displayName]