From 167b9884238d4d4e78ce52c455b89100423a4215 Mon Sep 17 00:00:00 2001 From: pezholio Date: Wed, 20 Nov 2024 14:51:23 +0000 Subject: [PATCH 1/6] Add `content_block_tools` gem --- Gemfile | 1 + Gemfile.lock | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index 2c2b7eb089c..c5361add5cd 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,7 @@ gem "bootsnap", require: false gem "carrierwave" gem "carrierwave-i18n" gem "chronic" +gem "content_block_tools" gem "dalli" gem "dartsass-rails" gem "diffy" diff --git a/Gemfile.lock b/Gemfile.lock index f93e078d5df..df956e7626a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -139,6 +139,8 @@ GEM coderay (1.1.3) concurrent-ruby (1.3.4) connection_pool (2.4.1) + content_block_tools (0.3.0) + actionview (>= 6, < 7.2.2) crack (1.0.0) bigdecimal rexml @@ -1028,6 +1030,7 @@ DEPENDENCIES carrierwave-i18n chronic climate_control + content_block_tools cucumber cucumber-rails dalli From b5ba2196fc6c0f46d32dbc8311f5912ce457a80f Mon Sep 17 00:00:00 2001 From: pezholio Date: Wed, 20 Nov 2024 15:00:58 +0000 Subject: [PATCH 2/6] Update Edition tests to use Minitest DSL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fits better with the pattern we’ve established that makes Minitest a bit more Rspec-like --- .../app/models/content_block_edition_test.rb | 151 ++++++++++-------- 1 file changed, 80 insertions(+), 71 deletions(-) diff --git a/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb b/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb index f99d41c018a..6b4946ca36a 100644 --- a/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb +++ b/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb @@ -1,122 +1,131 @@ require "test_helper" class ContentBlockManager::ContentBlockEditionTest < ActiveSupport::TestCase - setup do - @new_content_id = SecureRandom.uuid - ContentBlockManager::ContentBlock::Edition.any_instance.stubs(:create_random_id).returns(@new_content_id) - - @created_at = Time.zone.local(2000, 12, 31, 23, 59, 59).utc - @updated_at = Time.zone.local(2000, 12, 31, 23, 59, 59).utc - @details = { "some_field" => "some_content" } - @title = "Document title" - @creator = create(:user) - @organisation = create(:organisation) - - @content_block_edition = ContentBlockManager::ContentBlock::Edition.new( - created_at: @created_at, - updated_at: @updated_at, - details: @details, + extend Minitest::Spec::DSL + + let(:new_content_id) { SecureRandom.uuid } + + let(:created_at) { Time.zone.local(2000, 12, 31, 23, 59, 59).utc } + let(:updated_at) { Time.zone.local(2000, 12, 31, 23, 59, 59).utc } + let(:details) { { "some_field" => "some_content" } } + let(:title) { "Document title" } + let(:creator) { create(:user) } + let(:organisation) { create(:organisation) } + + let(:content_block_edition) do + ContentBlockManager::ContentBlock::Edition.new( + created_at: created_at, + updated_at: updated_at, + details: details, document_attributes: { block_type: "email_address", - title: @title, + title: title, }, - creator: @creator, - organisation_id: @organisation.id.to_s, + creator: creator, + organisation_id: organisation.id.to_s, ) - @content_block_edition.stubs(:schema).returns(build(:content_block_schema)) end - test "content_block_edition exists with required data" do - @content_block_edition.save! - @content_block_edition.reload + before do + ContentBlockManager::ContentBlock::Edition.any_instance.stubs(:create_random_id).returns(new_content_id) + content_block_edition.stubs(:schema).returns(build(:content_block_schema)) + end + + it "exists with required data" do + content_block_edition.save! + content_block_edition.reload - assert_equal @created_at, @content_block_edition.created_at - assert_equal @updated_at, @content_block_edition.updated_at - assert_equal @details, @content_block_edition.details + assert_equal created_at, content_block_edition.created_at + assert_equal updated_at, content_block_edition.updated_at + assert_equal details, content_block_edition.details end - test "it persists the block type to the document" do - @content_block_edition.save! - @content_block_edition.reload - document = @content_block_edition.document + it "persists the block type to the document" do + content_block_edition.save! + content_block_edition.reload + document = content_block_edition.document - assert_equal document.block_type, @content_block_edition.block_type + assert_equal document.block_type, content_block_edition.block_type end - test "it persists the title to the document" do - @content_block_edition.save! - @content_block_edition.reload - document = @content_block_edition.document + it "persists the title to the document" do + content_block_edition.save! + content_block_edition.reload + document = content_block_edition.document - assert_equal document.title, @content_block_edition.title + assert_equal document.title, content_block_edition.title end - test "it creates a document" do - @content_block_edition.save! - @content_block_edition.reload + it "creates a document" do + content_block_edition.save! + content_block_edition.reload - assert_not_nil @content_block_edition.document - assert_equal @content_block_edition.document.content_id, @new_content_id + assert_not_nil content_block_edition.document + assert_equal content_block_edition.document.content_id, new_content_id end - test "it adds a content id if a document is provided" do - @content_block_edition.document = build(:content_block_document, :email_address, content_id: nil) - @content_block_edition.save! - @content_block_edition.reload + it "adds a content id if a document is provided" do + content_block_edition.document = build(:content_block_document, :email_address, content_id: nil) + content_block_edition.save! + content_block_edition.reload - assert_not_nil @content_block_edition.document - assert_equal @content_block_edition.document.content_id, @new_content_id + assert_not_nil content_block_edition.document + assert_equal content_block_edition.document.content_id, new_content_id end - test "it validates the presence of a document block_type" do - @content_block_edition = build( + it "validates the presence of a document block_type" do + content_block_edition = build( :content_block_edition, - created_at: @created_at, - updated_at: @updated_at, - details: @details, + created_at: created_at, + updated_at: updated_at, + details: details, document_attributes: { block_type: nil, }, - organisation_id: @organisation.id.to_s, + organisation_id: organisation.id.to_s, ) - assert_invalid @content_block_edition - assert @content_block_edition.errors.full_messages.include?("Document block type can't be blank") + assert_invalid content_block_edition + assert content_block_edition.errors.full_messages.include?("Document block type can't be blank") end - test "it validates the presence of a document title" do + it "validates the presence of a document title" do content_block_edition = build( :content_block_edition, - created_at: @created_at, - updated_at: @updated_at, - details: @details, + created_at: created_at, + updated_at: updated_at, + details: details, document_attributes: { title: nil, }, - organisation_id: @organisation.id.to_s, + organisation_id: organisation.id.to_s, ) assert_invalid content_block_edition assert content_block_edition.errors.full_messages.include?("Title can't be blank") end - test "it adds a creator and first edition author for new records" do - @content_block_edition.save! - @content_block_edition.reload - assert_equal @content_block_edition.creator, @content_block_edition.edition_authors.first.user + it "adds a creator and first edition author for new records" do + content_block_edition.save! + content_block_edition.reload + assert_equal content_block_edition.creator, content_block_edition.edition_authors.first.user end - test "#creator= raises an exception if called for a persisted record" do - @content_block_edition.save! - assert_raise RuntimeError do - @content_block_edition.creator = create(:user) + describe "#creator=" do + it "raises an exception if called for a persisted record" do + content_block_edition.save! + assert_raise RuntimeError do + content_block_edition.creator = create(:user) + end end end - test "#update_document_reference_to_latest_edition! updates the document reference to the latest edition" do - latest_edition = create(:content_block_edition, document: @content_block_edition.document) - latest_edition.update_document_reference_to_latest_edition! + describe "#update_document_reference_to_latest_edition!" do + it "updates the document reference to the latest edition" do + latest_edition = create(:content_block_edition, document: content_block_edition.document) + latest_edition.update_document_reference_to_latest_edition! - assert_equal latest_edition.document.latest_edition_id, latest_edition.id + assert_equal latest_edition.document.latest_edition_id, latest_edition.id + end end end From 9c09b8b7e7f852d0ffc2509e45dac89a06050c4b Mon Sep 17 00:00:00 2001 From: pezholio Date: Wed, 20 Nov 2024 15:07:39 +0000 Subject: [PATCH 3/6] Add a `render` method to Content Block Editions This will make it easier to get a representation of a particular edition --- .../content_block/edition.rb | 9 +++++ .../app/models/content_block_edition_test.rb | 40 ++++++++++++++----- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/lib/engines/content_block_manager/app/models/content_block_manager/content_block/edition.rb b/lib/engines/content_block_manager/app/models/content_block_manager/content_block/edition.rb index 11840ae093a..1edcc938f19 100644 --- a/lib/engines/content_block_manager/app/models/content_block_manager/content_block/edition.rb +++ b/lib/engines/content_block_manager/app/models/content_block_manager/content_block/edition.rb @@ -11,6 +11,15 @@ class Edition < ApplicationRecord def update_document_reference_to_latest_edition! document.update!(latest_edition_id: id) end + + def render + ContentBlockTools::ContentBlock.new( + document_type: "content_block_#{block_type}", + content_id: document.content_id, + title:, + details:, + ).render + end end end end diff --git a/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb b/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb index 6b4946ca36a..3a6ef5da6c6 100644 --- a/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb +++ b/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb @@ -14,14 +14,14 @@ class ContentBlockManager::ContentBlockEditionTest < ActiveSupport::TestCase let(:content_block_edition) do ContentBlockManager::ContentBlock::Edition.new( - created_at: created_at, - updated_at: updated_at, - details: details, + created_at:, + updated_at:, + details:, document_attributes: { block_type: "email_address", - title: title, + title:, }, - creator: creator, + creator:, organisation_id: organisation.id.to_s, ) end @@ -76,9 +76,9 @@ class ContentBlockManager::ContentBlockEditionTest < ActiveSupport::TestCase it "validates the presence of a document block_type" do content_block_edition = build( :content_block_edition, - created_at: created_at, - updated_at: updated_at, - details: details, + created_at:, + updated_at:, + details:, document_attributes: { block_type: nil, }, @@ -92,9 +92,9 @@ class ContentBlockManager::ContentBlockEditionTest < ActiveSupport::TestCase it "validates the presence of a document title" do content_block_edition = build( :content_block_edition, - created_at: created_at, - updated_at: updated_at, - details: details, + created_at:, + updated_at:, + details:, document_attributes: { title: nil, }, @@ -128,4 +128,22 @@ class ContentBlockManager::ContentBlockEditionTest < ActiveSupport::TestCase assert_equal latest_edition.document.latest_edition_id, latest_edition.id end end + + describe "#render" do + let(:rendered_response) { "RENDERED" } + let(:stub_block) { stub("ContentBlockTools::ContentBlock", render: rendered_response) } + let(:document) { content_block_edition.document } + + it "initializes and renders a content block" do + ContentBlockTools::ContentBlock.expects(:new) + .with( + document_type: "content_block_#{document.block_type}", + content_id: document.content_id, + title:, + details:, + ).returns(stub_block) + + assert_equal content_block_edition.render, rendered_response + end + end end From fcdac918f5dca81c7e4b3d12bb82aa0a7b627d02 Mon Sep 17 00:00:00 2001 From: pezholio Date: Thu, 21 Nov 2024 13:08:53 +0000 Subject: [PATCH 4/6] Add a `current_versions` scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures we’re only querying editions that are the most current published version --- .../content_block_manager/content_block/edition.rb | 6 ++++++ .../unit/app/models/content_block_edition_test.rb | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/engines/content_block_manager/app/models/content_block_manager/content_block/edition.rb b/lib/engines/content_block_manager/app/models/content_block_manager/content_block/edition.rb index 1edcc938f19..e2143dc6f64 100644 --- a/lib/engines/content_block_manager/app/models/content_block_manager/content_block/edition.rb +++ b/lib/engines/content_block_manager/app/models/content_block_manager/content_block/edition.rb @@ -8,6 +8,12 @@ class Edition < ApplicationRecord include ValidatesDetails include Workflow + scope :current_versions, lambda { + joins( + "LEFT JOIN content_block_documents document ON document.latest_edition_id = content_block_editions.id", + ).where(state: "published") + } + def update_document_reference_to_latest_edition! document.update!(latest_edition_id: id) end diff --git a/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb b/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb index 3a6ef5da6c6..ed0bb2ad40e 100644 --- a/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb +++ b/lib/engines/content_block_manager/test/unit/app/models/content_block_edition_test.rb @@ -129,6 +129,18 @@ class ContentBlockManager::ContentBlockEditionTest < ActiveSupport::TestCase end end + describe ".current_versions" do + it "returns current published versions" do + document = create(:content_block_document, :email_address) + edition = create(:content_block_edition, :email_address, state: "published", document:) + draft_edition = create(:content_block_edition, :email_address, state: "draft", document:) + document.latest_edition = draft_edition + document.save! + + assert_equal ContentBlockManager::ContentBlock::Edition.current_versions.to_a, [edition] + end + end + describe "#render" do let(:rendered_response) { "RENDERED" } let(:stub_block) { stub("ContentBlockTools::ContentBlock", render: rendered_response) } From 0d885eddc6786597f245558250238e90c31b03b1 Mon Sep 17 00:00:00 2001 From: pezholio Date: Wed, 20 Nov 2024 16:00:53 +0000 Subject: [PATCH 5/6] Add a service to find and replace embed codes Although this will be used in Whitehall proper, it makes sense to have this namespaced under the ContentBlockManager --- .../find_and_replace_embed_codes_service.rb | 35 +++++++++++++ ...nd_and_replace_embed_codes_service_test.rb | 51 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 lib/engines/content_block_manager/app/services/content_block_manager/find_and_replace_embed_codes_service.rb create mode 100644 lib/engines/content_block_manager/test/unit/app/services/find_and_replace_embed_codes_service_test.rb diff --git a/lib/engines/content_block_manager/app/services/content_block_manager/find_and_replace_embed_codes_service.rb b/lib/engines/content_block_manager/app/services/content_block_manager/find_and_replace_embed_codes_service.rb new file mode 100644 index 00000000000..62d91a0f014 --- /dev/null +++ b/lib/engines/content_block_manager/app/services/content_block_manager/find_and_replace_embed_codes_service.rb @@ -0,0 +1,35 @@ +module ContentBlockManager + class FindAndReplaceEmbedCodesService + def self.call(html) + new(html).call + end + + def call + embed_content_references.each do |reference| + content_block = content_blocks.find { |c| c.document.content_id == reference.content_id } + next if content_block.nil? + + html.gsub!(reference.embed_code, content_block.render) + end + + html + end + + private + + attr_reader :html + + def initialize(html) + @html = html + end + + def embed_content_references + @embed_content_references ||= ContentBlockTools::ContentBlockReference.find_all_in_document(html) + end + + def content_blocks + @content_blocks ||= ContentBlockManager::ContentBlock::Edition.current_versions + .where(document: { content_id: embed_content_references.map(&:content_id) }) + end + end +end diff --git a/lib/engines/content_block_manager/test/unit/app/services/find_and_replace_embed_codes_service_test.rb b/lib/engines/content_block_manager/test/unit/app/services/find_and_replace_embed_codes_service_test.rb new file mode 100644 index 00000000000..7ba41253ed0 --- /dev/null +++ b/lib/engines/content_block_manager/test/unit/app/services/find_and_replace_embed_codes_service_test.rb @@ -0,0 +1,51 @@ +require "test_helper" + +class ContentBlockManager::FindAndReplaceEmbedCodesServiceTest < ActiveSupport::TestCase + extend Minitest::Spec::DSL + + it "finds and replaces embed codes" do + document_1 = create(:content_block_document, :email_address) + edition_1 = create(:content_block_edition, :email_address, state: "published", document: document_1) + document_1.latest_edition = edition_1 + document_1.save! + + document_2 = create(:content_block_document, :email_address) + edition_2 = create(:content_block_edition, :email_address, state: "published", document: document_2) + document_2.latest_edition = edition_2 + document_2.save! + + html = """ +

Hello there

+

#{edition_2.document.embed_code}

+

#{edition_1.document.embed_code}

+ """ + + expected = """ +

Hello there

+

#{edition_2.render}

+

#{edition_1.render}

+ """ + + result = ContentBlockManager::FindAndReplaceEmbedCodesService.call(html) + + assert_equal result, expected + end + + it "ignores blocks that aren't present in the database" do + edition = build(:content_block_edition, :email_address) + + html = edition.document.embed_code + + result = ContentBlockManager::FindAndReplaceEmbedCodesService.call(html) + assert_equal result, html + end + + it "ignores blocks that don't have a live version" do + edition = create(:content_block_edition, :email_address, state: "draft") + + html = edition.document.embed_code + + result = ContentBlockManager::FindAndReplaceEmbedCodesService.call(html) + assert_equal result, html + end +end From 50acb8baa46c0fe3da0872486421ad53bc6b922e Mon Sep 17 00:00:00 2001 From: pezholio Date: Wed, 20 Nov 2024 17:07:44 +0000 Subject: [PATCH 6/6] Call Embed codes service when previewing This updates `govspeak_to_admin_html` to call the new service on any generated HTML, so embed codes are transformed in the live preview. --- app/helpers/admin/admin_govspeak_helper.rb | 3 ++- .../app/helpers/admin/admin_govspeak_helper_test.rb | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/helpers/admin/admin_govspeak_helper.rb b/app/helpers/admin/admin_govspeak_helper.rb index 5448f0add25..157d1777aed 100644 --- a/app/helpers/admin/admin_govspeak_helper.rb +++ b/app/helpers/admin/admin_govspeak_helper.rb @@ -4,7 +4,8 @@ module Admin::AdminGovspeakHelper def govspeak_to_admin_html(govspeak, images = [], attachments = [], alternative_format_contact_email = nil) images = prepare_images(images) attachments = prepare_attachments(attachments, alternative_format_contact_email) - wrapped_in_govspeak_div(bare_govspeak_to_admin_html(govspeak, images, attachments)) + html = ContentBlockManager::FindAndReplaceEmbedCodesService.call bare_govspeak_to_admin_html(govspeak, images, attachments) + wrapped_in_govspeak_div(html) end def govspeak_edition_to_admin_html(edition) diff --git a/test/unit/app/helpers/admin/admin_govspeak_helper_test.rb b/test/unit/app/helpers/admin/admin_govspeak_helper_test.rb index a10cc0f0cd2..d19fa13e428 100644 --- a/test/unit/app/helpers/admin/admin_govspeak_helper_test.rb +++ b/test/unit/app/helpers/admin/admin_govspeak_helper_test.rb @@ -121,4 +121,14 @@ class Admin::AdminGovspeakHelperTest < ActionView::TestCase assert_equivalent_html "
#{contact_html}
", govspeak_to_admin_html(input) end end + + test "should call the embed codes helper" do + input = "Here is some Govspeak" + expected = "Expected output" + ContentBlockManager::FindAndReplaceEmbedCodesService.expects(:call).with( + bare_govspeak_to_admin_html(input, [], []), + ).returns(expected) + + assert_equivalent_html "
#{expected}
", govspeak_to_admin_html(input) + end end