Skip to content

Commit

Permalink
Merge pull request #2813 from alphagov/embedded-content-v2
Browse files Browse the repository at this point in the history
Allow embedding of content within document body
  • Loading branch information
brucebolt authored Aug 7, 2024
2 parents b2bf145 + 0cf721c commit e96a7d1
Show file tree
Hide file tree
Showing 11 changed files with 540 additions and 38 deletions.
10 changes: 8 additions & 2 deletions app/commands/v2/put_content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ def check_update_type
end

def create_links(edition)
return if payload[:links].nil?
links = payload.fetch(:links, {}).merge({ embed: fetch_embedded_content(edition) })

payload[:links].each do |link_type, target_link_ids|
links.each do |link_type, target_link_ids|
edition.links.create!(
target_link_ids.map.with_index do |target_link_id, i|
{ link_type:, target_content_id: target_link_id, position: i }
Expand All @@ -98,6 +98,12 @@ def create_links(edition)
end
end

def fetch_embedded_content(edition)
return [] if edition[:details]["body"].nil?

EmbeddedContentFinderService.new.fetch_linked_content_ids(edition[:details]["body"], edition.locale)
end

def create_redirect
return unless payload[:base_path]

Expand Down
54 changes: 54 additions & 0 deletions app/presenters/content_embed_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module Presenters
class ContentEmbedPresenter
def initialize(edition)
@edition = edition
end

def render_embedded_content(details)
return details if details[:body].nil?

details[:body] = if details[:body].is_a?(Array)
details[:body].map do |content|
{
content_type: content[:content_type],
content: render_embedded_editions(content[:content]),
}
end
else
render_embedded_editions(details[:body])
end

details
end

private

def render_embedded_editions(content)
embedded_content_references = EmbeddedContentFinderService.new.find_content_references(content)
embedded_content_references_by_content_id = embedded_content_references.index_by(&:content_id)

target_content_ids = @edition
.links
.where(link_type: "embed")
.pluck(:target_content_id)

embedded_edition_ids = ::Queries::GetEditionIdsWithFallbacks.call(
target_content_ids,
locale_fallback_order: [@edition.locale, Edition::DEFAULT_LOCALE].uniq,
state_fallback_order: %w[published],
)

embedded_editions = Edition.where(id: embedded_edition_ids)

embedded_editions.each do |embedded_edition|
embed_code = embedded_content_references_by_content_id[embedded_edition.content_id].embed_code
content = content.gsub(
embed_code,
embedded_edition.title,
)
end

content
end
end
end
12 changes: 9 additions & 3 deletions app/presenters/details_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

module Presenters
class DetailsPresenter
attr_reader :content_item_details, :change_history_presenter
attr_reader :content_item_details, :change_history_presenter, :content_embed_presenter

def initialize(content_item_details, change_history_presenter)
def initialize(content_item_details, change_history_presenter, content_embed_presenter)
@content_item_details = SymbolizeJSON.symbolize(content_item_details)
@change_history_presenter = change_history_presenter
@content_embed_presenter = content_embed_presenter
end

def details
@details ||=
begin
updated = recursively_transform_govspeak(content_item_details)
updated = content_embed(content_item_details).presence || content_item_details
updated = recursively_transform_govspeak(updated)
updated[:change_history] = change_history if change_history.present?
updated
end
Expand Down Expand Up @@ -46,6 +48,10 @@ def recursively_transform_govspeak(obj)
end
end

def content_embed(content_item_details)
@content_embed ||= content_embed_presenter&.render_embedded_content(content_item_details)
end

def change_history
@change_history ||= change_history_presenter&.change_history
end
Expand Down
6 changes: 6 additions & 0 deletions app/presenters/edition_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,15 @@ def details_presenter
@details_presenter ||= Presenters::DetailsPresenter.new(
edition.to_h[:details],
change_history_presenter,
content_embed_presenter,
)
end

def content_embed_presenter
@content_embed_presenter ||=
Presenters::ContentEmbedPresenter.new(edition)
end

def change_history_presenter
@change_history_presenter ||=
Presenters::ChangeHistoryPresenter.new(edition)
Expand Down
7 changes: 6 additions & 1 deletion app/presenters/queries/expanded_link_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def present_expanded_link(link_hash)
end

if hash[:details]
hash[:details] = Presenters::DetailsPresenter.new(hash[:details], nil).details
hash[:details] = Presenters::DetailsPresenter.new(hash[:details], nil, content_embed_presenter(hash[:content_id], hash[:locale])).details
end
end
end
Expand All @@ -77,6 +77,11 @@ def available_translations
def translations
available_translations.translations
end

def content_embed_presenter(content_id, locale)
edition = Document.find_by(content_id:, locale:).live
Presenters::ContentEmbedPresenter.new(edition) if edition
end
end
end
end
45 changes: 45 additions & 0 deletions app/services/embedded_content_finder_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
class EmbeddedContentFinderService
ContentReference = Data.define(:document_type, :content_id, :embed_code)

SUPPORTED_DOCUMENT_TYPES = %w[contact].freeze
UUID_REGEX = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/
EMBED_REGEX = /({{embed:(#{SUPPORTED_DOCUMENT_TYPES.join('|')}):#{UUID_REGEX}}})/

def fetch_linked_content_ids(body, locale)
content_references = if body.is_a?(Array)
body.map { |hash| find_content_references(hash[:content]) }.flatten
else
find_content_references(body)
end
return [] if content_references.empty?

check_all_references_exist(content_references, locale)
content_references.map(&:content_id)
end

def find_content_references(body)
body.scan(EMBED_REGEX).map { |match| ContentReference.new(document_type: match[1], content_id: match[2], embed_code: match[0]) }.uniq
end

private

def check_all_references_exist(content_references, locale)
found_editions = live_editions(content_references, locale)
if found_editions.count != content_references.count
not_found_content_ids = content_references.map(&:content_id) - found_editions.map(&:content_id)
raise CommandError.new(
code: 422,
message: "Could not find any live editions in locale #{locale} for: #{not_found_content_ids.join(', ')}, ",
)
end
end

def live_editions(content_references, locale)
Edition.with_document.where(
state: "published",
content_store: "live",
document_type: content_references.map(&:document_type),
documents: { content_id: content_references.map(&:content_id), locale: },
)
end
end
106 changes: 106 additions & 0 deletions spec/integration/put_content/content_with_embedded_content_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
RSpec.describe "PUT /v2/content when embedded content is provided" do
include_context "PutContent call"

context "with embedded content" do
let(:first_contact) { create(:edition, state: "published", content_store: "live", document_type: "contact") }
let(:second_contact) { create(:edition, state: "published", content_store: "live", document_type: "contact") }
let(:document) { create(:document, content_id:) }

before do
payload.merge!(document_type: "press_release", schema_name: "news_article", details: { body: "{{embed:contact:#{first_contact.document.content_id}}} {{embed:contact:#{second_contact.document.content_id}}}" })
end

it "should create links" do
expect {
put "/v2/content/#{content_id}", params: payload.to_json
}.to change(Link, :count).by(2)

expect(Link.find_by(target_content_id: first_contact.content_id)).not_to be_nil
expect(Link.find_by(target_content_id: second_contact.content_id)).not_to be_nil
end
end

context "without embedded content and embed links already existing on a draft edition" do
let(:contact) { create(:edition, state: "published", content_store: "live", document_type: "contact") }
let(:document) { create(:document, content_id:) }
let(:edition) { create(:edition, document:) }

before do
stub_request(:put, %r{.*content-store.*/content/.*})
edition.links.create!({
link_type: "embed",
target_content_id: contact.content_id,
position: 0,
})
payload.merge!(document_type: "press_release", schema_name: "news_article", details: { body: "no embed links" })
end

it "should remove embed links" do
expect {
put "/v2/content/#{content_id}", params: payload.to_json
}.to change(Link, :count).by(-1)

expect(Link.find_by(target_content_id: contact.content_id)).to be_nil
end
end

context "with different embedded content and embed links already existing on a draft edition" do
let(:first_contact) { create(:edition, state: "published", content_store: "live", document_type: "contact") }
let(:second_contact) { create(:edition, state: "published", content_store: "live", document_type: "contact") }
let(:document) { create(:document, content_id:) }
let(:edition) { create(:edition, document:) }

before do
stub_request(:put, %r{.*content-store.*/content/.*})
edition.links.create!({
link_type: "embed",
target_content_id: first_contact.content_id,
position: 0,
})
payload.merge!(document_type: "press_release", schema_name: "news_article", details: { body: "{{embed:contact:#{second_contact.document.content_id}}}" })
end

it "should replace the embed link" do
expect {
put "/v2/content/#{content_id}", params: payload.to_json
}.to change(Link, :count).by(0)

expect(Link.find_by(target_content_id: first_contact.content_id)).to be_nil
expect(Link.find_by(target_content_id: second_contact.content_id)).not_to be_nil
end
end

context "with embedded content that does not exist" do
let(:document) { create(:document, content_id:) }
let(:fake_content_id) { SecureRandom.uuid }

before do
payload.merge!(document_type: "press_release", schema_name: "news_article", details: { body: "{{embed:contact:#{fake_content_id}}}" })
end

it "should return a 422 error" do
put "/v2/content/#{content_id}", params: payload.to_json

expect(response).to be_unprocessable
expect(response.body).to match(/Could not find any live editions in locale en for: #{fake_content_id}/)
end
end

context "with a mixture of embedded content that does and does not exist" do
let(:contact) { create(:edition, state: "published", content_store: "live", document_type: "contact") }
let(:document) { create(:document, content_id:) }
let(:first_fake_content_id) { SecureRandom.uuid }
let(:second_fake_content_id) { SecureRandom.uuid }

before do
payload.merge!(document_type: "press_release", schema_name: "news_article", details: { body: "{{embed:contact:#{contact.document.content_id}}} {{embed:contact:#{first_fake_content_id}}} {{embed:contact:#{second_fake_content_id}}}" })
end

it "should return a 422 error" do
put "/v2/content/#{content_id}", params: payload.to_json

expect(response).to be_unprocessable
expect(response.body).to match(/Could not find any live editions in locale en for: #{first_fake_content_id}, #{second_fake_content_id}/)
end
end
end
Loading

0 comments on commit e96a7d1

Please sign in to comment.