Skip to content

Implement StandardEdition Parent/Child relationships with semi-independent publishing workflows [WHIT-3341]#11431

Draft
ChrisBAshton wants to merge 19 commits into
mainfrom
spike-independently-editable-standardedition-children
Draft

Implement StandardEdition Parent/Child relationships with semi-independent publishing workflows [WHIT-3341]#11431
ChrisBAshton wants to merge 19 commits into
mainfrom
spike-independently-editable-standardedition-children

Conversation

@ChrisBAshton
Copy link
Copy Markdown
Contributor

@ChrisBAshton ChrisBAshton commented May 7, 2026

What

This PR implements a generic multi-page architecture and applies it to Topical Events and their 'About' pages (dependent on alphagov/publishing-api#3860).

Child documents have largely independent publishing workflows, by design - this allows publishers to make changes to their child documents without having to do everything through the parent. They can't be totally independent though, otherwise we could end up with orphaned children, live children with unpublished parents and so on. So we've encoded four simple, sensible rules that constrain the lifecycles and help keep states predictable and valid. See Mural for some of the thinking.

Rules

Rule 1: New child drafts

A child document can only be created on a parent in a pre-publication state.

Rule 2: Prevent orphaned children

A parent draft may not be discarded while it has associated child documents whose only edition is linked to that parent draft.

We could offer to drop the draft documents automatically when dropping the parent. But the simpler solution for now is to raise a validation error and force the publisher to drop the child documents manually first.

Rule 3: Child publishability

A draft child may only be published if its parent document’s live_edition is "published" and the parent edition it is linked to is not in a pre-publication state.

The simplest implementation, applied here, is to force the publisher to publish the parent (draft) document, then publish the child document.

In future, I expect we'd offer some sort of "publish all (or a subset of) draft children with the parent" feature in future, where the operation order would be that the parent is published first, thereby allowing said children to be published in turn. However, whilst we can surface that kind of UX on the Publish / Force-Publish pages, we can't do the equivalent for schedule-publishing until we tackle WHIT-1887.

Rule 4: Parent visibility ceiling

A child document may not be more publicly visible than its parent document.

Parent Allowed child states
published published / withdrawn / unpublished
withdrawn withdrawn / unpublished
unpublished unpublished

Implementation wise, we could auto-cascade such that if you unpublish the parent, you unpublish its children. But it gets messy if there exists a draft child - do you auto-discard the draft in order to unpublish it? Etc. So to begin with at least, we will force the publisher to manually withdraw/unpublish (as appropriate) all of a document's children before withdrawing/unpublishing the parent document. This simplified interpretation can be summarised as "You cannot withdraw/unpublish a parent while any children remain more publicly visible than it."

Out of scope (future tickets)

  • Limiting the number of About pages that can be created.
  • (if deemed necessary) Hard-coding the slug to "/about", rather than having it determined by title.
  • Redirect locations. Currently when publishing a Child document, you're redirected to the Document Search screen, but should probably be redirected to the Parent summary screen instead? Same with the cancel link on the "New child document" form.
  • Hide Topical Event About pages from Whitehall search (if needed)
  • Offer option to - or just go ahead and action - deletion of 'new' child document when deleting its draft parent. (See Rule 2)
  • Offer option to publish all or a subset of child pages at the same time as the parent (see Rule 3)
  • "Through the parent" publishing, e.g. Publications & HTML Attachments, where the child document is 'part of the whole' and has a shared change history, shared last_updated timestamp, can't be edited independently, etc. Architecturally this should be possible by writing some additional constraints and config options on top of what we have here.

Why

Whitehall has committed to migrating Topical Events in their entirety, to our config-driven 'StandardEdition' model. Unlike the already migrated content types, Topical Events aren't standalone content items - many of them have corresponding 'About' pages too, which need migrating.

Looking more widely, there are other microsite-like and multi-part document structures we hope to consolidate into StandardEdition in future, including:

  • Publications (and HTML attachments)
  • Plan for Change pages (and their children)
  • Organisations (and their Corporate Information Pages)
  • Travel Advice (summary and individual sections)
  • Manuals (and manual sections)

So we've had to implement a multi-part architecture that can scale to accommodate all of those content types and their requirements. As in the 'out of scope' section, we're likely to need to add some configuration options and support 'top-down' / 'through-the-parent' publishing for some of them. We're also likely to need to introduce the concept of 'ordered' child pages (e.g. for HTML attachments). None of what we have built here locks us out from those future iterations.

Jira: https://gov-uk.atlassian.net/browse/WHIT-3341

Screenshots

We have a new "Child documents" section (hidden behind a feature flag) on Topical Event document summary pages:

Screenshot 2026-05-14 at 16 07 25

Creating a new child document takes you to a screen where you can choose from the subset of allowed child document types:

Screenshot 2026-05-14 at 16 08 26

You're taken to a StandardEdition new document form as normal - the only difference being the additional call-out reinforcing that this is being created as a child document of the given parent:

Screenshot 2026-05-14 at 16 08 43

The newly created document is again, just a StandardEdition - but with a callout at the top of the page, linking it back to the parent. Note that the publish/force-publish button is deliberately hidden, since we need the parent to be published before we allow the 'first publish' of the child (subsequent publishes are unrestricted).

(Note also the warning about the same title/slug - a growing problem and a symptom of the fact our global uniqueness check logic isn't compatible with consolidating onto StandardEdition, as we check document_type and slug - in this case 'StandardEdition' and 'About' - and unnecessarily add a --2/--3/... suffix, as the base path prefix here is actually unique. This will be tackled in WHIT-3371.)

Screenshot 2026-05-14 at 16 09 30 note validation error and lack of publish button

The child document now appears on the parent document summary screen:

Screenshot 2026-05-14 at 16 10 18

After publishing the parent, the callout text at the top of the child document is now simplified, and we can see the publish actions in the sidebar:

Screenshot 2026-05-14 at 16 12 16 after publishing parent

After publishing the child document, it is visible in Whitehall Search. We may want to change this (out of scope for this PR):

Screenshot 2026-05-14 at 16 14 35

⚠️ This repo is Continuously Deployed: make sure you follow the guidance ⚠️

This application is owned by the Whitehall Experience team. Please let us know in #govuk-whitehall-experience-tech when you raise any PRs.

Follow these steps if you are doing a Rails upgrade.

@ChrisBAshton ChrisBAshton force-pushed the spike-independently-editable-standardedition-children branch 11 times, most recently from 5731b5c to 62dc978 Compare May 8, 2026 16:29
@ChrisBAshton ChrisBAshton changed the title Spike independently editable StandardEdition Parent/Child relationships [WHIT-3341] Implement StandardEdition Parent/Child relationships with semi-independent publishing workflows [WHIT-3341] May 8, 2026
@ChrisBAshton ChrisBAshton force-pushed the spike-independently-editable-standardedition-children branch 17 times, most recently from f30dcce to 8fc1b77 Compare May 14, 2026 15:02
We're going to start modelling the concept of "parent" and "child"
documents, e.g. Topical Event pages (parent) and Topical Event
About pages (child). Parent/child docs are defined as having:

- A shared base path prefix (e.g. `/topical-events/foo` and
  `/topical-events/foo/about`)
- Interlinked publishing workflows, e.g. we may want to publish
  both parent and child at the same time, we may need to consolidate
  their change notes, coordinate their 'last updated' time, etc.
  That behaviour is TBC and is likely to be config-driven.

We could have modelled this in several different ways, each with
their own pros and cons:

1. Document -> Document
  - Simplest to model. No ongoing updates required when we create
    new editions of either parent or child.
  - But lossy. No way of knowing which edition of the parent a
    child was created on, etc.
2. Edition -> Document
  - Reasonably simple to model. Parent-centric view of child
    document creation. Every time a new edition of the parent is
    created, we create a new row in ParentChildRelationships, but
    otherwise we don't need to have equivalent callbacks when
    creating a new edition of the Child.
3. Edition -> Edition
  - Maximally flexible, but difficult to comprehend. Rows need
    creating whenever a parent or child edition is created, and
    kept in sync when discarding drafts etc,
4. Document -> Edition
  - Not really any advantage here. Lossy _and_ overly complicated.

Approach 1 and 3 were spiked in #11026. We've gone for Approach 2
here.

Note that we will one day likely need an 'ordering' attribute too,
for having ordered HTML attachments on a Publication. But that is
YAGNI for now.
This will make querying a document's parent or child much easier.
See subsequent commits.
We need to recreate the ParentChildRelationship when a new edition
of a Parent is created. We don't need to do the same when a new
edition of a _Child_ is created, since we reference the child by
its Document ID rather than its Edition ID.
This method looks up the document types that are allowed to be
a child of the given document, based on its configuration. We will
call this method when rendering the child document creation form.
We'll direct publishers to this page when they choose to create
a child document on the Parent document they're in. See subsequent
commits.
- Adds Topical Event About Page config
- Adds `allowed_child_document_types` to Topical Event
- Tightens up schema validation to allow no unexpected properties
  in `settings`
- Extracts enum values for document type, so we can avoid duplicating
  the list and we can validate both settings.publishing_api_document_type
  and allowed_child_document_types[n].document_type use only valid
  document types.
- Removes unnecessary unit test that was basically just testing
  the implementation of JSON schema enum validation.

Note that, as things currently stand, Publishing API rejects us
sending organisations as part of details or links for Topical Event
About pages - so we've deliberately omitted them here, as per the
legacy experience.
Non StandardEdition editions always return false here, as we only
support Parent/Child relationships on StandardEdition constructs.

StandardEdition editions explicitly support child documents if
we've provided a list of `allowed_child_document_types` in their
config.
We'll set the latter on topical event about pages. This is because
the base path prefix will be determined by the parent.

'is_child_document' as a property won't be used for the time being.
This is more as a signal that the absence of a 'base_path_prefix'
property is NOT a mistake, where otherwise it would be impossible
to tell without looking at the `allowed_child_document_types`
value of every other document type. In future we may find it
useful to be able to look up the `is_child_document` boolean.
Note: Topical Event About pages (legacy) have a hard-coded slug
of "/about". This commit fixes the prefix so that it will come
out as "/government/topical-events/{the-topical-event}/{name-of-doc}",
but the {name-of-doc} bit is currently still dynamic. We can look
at that in a future commit if it's deemed essential.
Starts a feature file describing creating a child document from the
parent.

Over the next few commits, we'll introduce the agreed business
rules so that we can keep the documents in a reasonable synchronised
state (avoiding orphaned children, or children that are live when
the parent is unpublished, and so on).

NB: I used the same design for surfacing Child Documents on the
document summary page, as exists for listing corporate information
pages on Organisations, since it seems useful to surface the state
of the child documents here too. It uses the same 'fake edition
filter' approach as that.
A child document can only be created on a parent in a pre-publication state.
We'll use this to implement Rule 2.
Rule 2 is to disallow publishers from discarding a draft parent if
it contains a draft child that has never been published (in order
to avoid creating an orphaned child).
But before we do that, we need to make sure if the publisher deletes the child, that
validation error doesn't happen! This commit accomplishes that.
A parent draft may not be discarded while it has associated child documents whose only edition is linked to that parent draft.

We could offer to drop the draft documents automatically when dropping the parent. But the simpler solution for now is to raise a validation error and force the publisher to drop the child documents manually first.
A draft child may only be published if its parent document’s live_edition is "published" and its associated parent edition is not in a pre-publication state.

The simplest implementation is to force the publisher to publish
the parent (draft) document, then publish the child document. But I
expect before too long we'll offer some sort of "publish all (or a
subset of) draft children with the parent" feature in future, where
the operation order would be that the parent is published first,
thereby allowing said children to be published in turn.
A child document may not be more publicly visible than its parent document.

| Parent      | Allowed child states                |
|-------------|-------------------------------------|
| published   | published / withdrawn / unpublished |
| withdrawn   | withdrawn / unpublished             |
| unpublished | unpublished                         |

Implementation wise, we **could** auto-cascade such that if you unpublish the parent, you unpublish its children. But it gets messy if there exists a draft child - do you auto-discard the draft in order to unpublish it? Etc. So to begin with at least, we will force the publisher to manually withdraw/unpublish (as appropriate) all of a document's children before withdrawing/unpublishing the parent document. This simplified interpretation can be summarised as "You cannot withdraw/unpublish a parent while any children remain more publicly visible than it."
@ChrisBAshton ChrisBAshton force-pushed the spike-independently-editable-standardedition-children branch from 8fc1b77 to 05818d2 Compare May 14, 2026 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant