Skip to content
This repository was archived by the owner on Oct 27, 2025. It is now read-only.

Commit e8aa02b

Browse files
committed
WIP: Add 'document_history' property
Whitehall stores change notes and editorial remarks _per edition_, yet we're only planning to migrate the latest (published) edition out of Content Publisher, for simplicity. So we imagine this property will be used to form one large 'editorial remark' in Whitehall (and potentially as one large entry in the public changenote history too). The TimelineEntry class is doing the heavy lifting here. I've generally followed the same logic as _content_publisher_entry.html.erb in deciding which properties to expose. The way we're testing this behaviour (by layering edition upon edition in a unit test) doesn't appear to have been done before, as we were getting an Uninitialized Constant error on `RevisionUpdater::Image`. Adding a `require_relative` to the `RevisionUpdater` class worked around that.
1 parent a351f80 commit e8aa02b

3 files changed

Lines changed: 163 additions & 0 deletions

File tree

app/models/whitehall_migration/document_export.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,32 @@ def self.export_to_hash(document)
2424
body: content_revision.contents["body"],
2525
tags: document.live_edition.revision.tags_revision.tags,
2626
political: document.live_edition.political?,
27+
document_history: document_history(document),
2728
}
2829
end
30+
31+
def self.document_history(document)
32+
timeline_entries = TimelineEntry.where(document:)
33+
.includes(:created_by, :details)
34+
.order(created_at: :desc)
35+
.includes(:edition)
36+
37+
timeline_entries.map do |entry|
38+
entry_content = if entry.internal_note? && entry.details
39+
entry.details.body
40+
elsif (entry.withdrawn? || entry.withdrawn_updated?) && entry.details
41+
entry.details.public_explanation
42+
end
43+
44+
{
45+
edition_number: entry.edition.number,
46+
entry_type: entry.entry_type,
47+
date: entry.created_at.to_fs(:date),
48+
time: entry.created_at.to_fs(:time),
49+
backdated_to: entry.backdated? ? entry.revision.backdated_to.to_fs(:date) : nil,
50+
user: entry.created_by.email,
51+
entry_content:,
52+
}
53+
end
54+
end
2955
end

lib/versioning/revision_updater.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module Versioning
22
class RevisionUpdater < BaseUpdater
3+
require_relative "./revision_updater/image"
34
include RevisionUpdater::Image
45
include RevisionUpdater::FileAttachment
56

spec/models/whitehall_migration/document_export_spec.rb

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,5 +113,141 @@
113113

114114
expect(described_class.export_to_hash(document)[:political]).to be(true)
115115
end
116+
117+
describe "the `document_history` property" do
118+
let(:document) { instance_double(Document) }
119+
120+
it "includes internal notes" do
121+
details = instance_double(InternalNote, body: "This is an internal note")
122+
e = entry_double(
123+
internal_note?: true,
124+
details:,
125+
entry_type: "internal_note",
126+
edition: instance_double(Edition, number: 1),
127+
created_by: instance_double(User, email: "example@gov.uk"),
128+
)
129+
stub_chain_with([e])
130+
131+
expect(described_class.document_history(document)).to eq([
132+
{
133+
edition_number: 1,
134+
entry_type: "internal_note",
135+
date: "2024-01-01",
136+
time: "10:00",
137+
backdated_to: nil,
138+
user: "example@gov.uk",
139+
entry_content: "This is an internal note",
140+
},
141+
])
142+
end
143+
144+
it "includes withdrawn/updated entries with public explanation" do
145+
details = instance_double(Withdrawal, public_explanation: "Withdrawn explanation")
146+
e = entry_double(
147+
withdrawn_updated?: true,
148+
details:,
149+
entry_type: "withdrawn",
150+
edition: instance_double(Edition, number: 2),
151+
created_at: build_time(date: "2024-02-01", time: "11:00"),
152+
created_by: instance_double(User, email: "withdrawn-author@gov.uk"),
153+
)
154+
stub_chain_with([e])
155+
156+
expect(described_class.document_history(document)).to eq([
157+
{
158+
edition_number: 2,
159+
entry_type: "withdrawn",
160+
date: "2024-02-01",
161+
time: "11:00",
162+
backdated_to: nil,
163+
user: "withdrawn-author@gov.uk",
164+
entry_content: "Withdrawn explanation",
165+
},
166+
])
167+
end
168+
169+
it "includes backdated entries (backdated_to on revision) with nil entry_content" do
170+
backdated_to = Date.new(2022, 1, 1) # real Date; .to_fs(:date) -> "2022-01-01"
171+
revision = instance_double(Revision, backdated_to:)
172+
173+
e = entry_double(
174+
backdated?: true,
175+
revision:,
176+
entry_type: "published",
177+
edition: instance_double(Edition, number: 5),
178+
created_at: build_time(date: "2024-05-01", time: "14:00"),
179+
created_by: instance_double(User, email: "backdated-author@gov.uk"),
180+
)
181+
stub_chain_with([e])
182+
183+
expect(described_class.document_history(document)).to eq([
184+
{
185+
edition_number: 5,
186+
entry_type: "published",
187+
date: "2024-05-01",
188+
time: "14:00",
189+
backdated_to: "2022-01-01",
190+
user: "backdated-author@gov.uk",
191+
entry_content: nil,
192+
},
193+
])
194+
end
195+
196+
it "returns entries ordered by created_at desc (we respect the chain's order)" do
197+
newer = entry_double(
198+
entry_type: "withdrawn",
199+
withdrawn_updated?: true,
200+
details: instance_double(Withdrawal, public_explanation: "Later"),
201+
edition: instance_double(Edition, number: 2),
202+
created_at: build_time(date: "2024-02-01", time: "11:00"),
203+
created_by: instance_double(User, email: "b@gov.uk"),
204+
)
205+
206+
older = entry_double(
207+
internal_note?: true,
208+
details: instance_double(InternalNote, body: "Earlier note"),
209+
entry_type: "internal_note",
210+
edition: instance_double(Edition, number: 1),
211+
created_at: build_time(date: "2024-01-01", time: "10:00"),
212+
created_by: instance_double(User, email: "a@gov.uk"),
213+
)
214+
215+
# We hand back [newer, older] from the ordered chain
216+
stub_chain_with([newer, older])
217+
218+
result = described_class.document_history(document)
219+
expect(result.map { |h| h[:edition_number] }).to eq([2, 1])
220+
end
221+
222+
def build_time(date:, time:)
223+
t = instance_double(Time)
224+
allow(t).to receive(:to_fs).with(:date).and_return(date)
225+
allow(t).to receive(:to_fs).with(:time).and_return(time)
226+
t
227+
end
228+
229+
def stub_chain_with(entries)
230+
# Simulate: TimelineEntry.where(document: doc).includes(...).order(...).includes(...)
231+
allow(TimelineEntry).to receive(:where).with(document:).and_return(entries)
232+
allow(entries).to receive(:includes).and_return(entries)
233+
allow(entries).to receive(:order).with(created_at: :desc).and_return(entries)
234+
end
235+
236+
def entry_double(overrides = {})
237+
defaults = {
238+
edition: instance_double(Edition, number: 1),
239+
entry_type: "internal_note",
240+
created_at: build_time(date: "2024-01-01", time: "10:00"),
241+
created_by: instance_double(User, email: "example@gov.uk"),
242+
details: nil,
243+
internal_note?: false,
244+
withdrawn?: false,
245+
withdrawn_updated?: false,
246+
backdated?: false,
247+
revision: nil,
248+
}
249+
instance_double(TimelineEntry, **defaults.merge(overrides))
250+
end
251+
end
116252
end
117253
end

0 commit comments

Comments
 (0)