task/CDD 1379 page previews#3077
Conversation
46d2401 to
23231b1
Compare
0ee49d3 to
258dd9a
Compare
jrdh
left a comment
There was a problem hiding this comment.
I've made lots of comments but this is looking really good! Worked nicely on my local machine 🙂
258dd9a to
3a013f1
Compare
aacf010 to
93f696d
Compare
|
93f696d to
fefbfc5
Compare
e15a8fe to
06661aa
Compare
jrdh
left a comment
There was a problem hiding this comment.
Still looking good. Made a few comments in line but nothing major there. I have concern which I think we have to fix and a question.
The concern is that I think the /nocache/ functionality should be protected by the same method we're using for the /preview/ URLs (i.e. signing) as without any authentication on them they allow access to potentially unembargoed data and open up endpoints malicious users could DDOS us with due to the uncached nature of the responses.
The question is whether you considered using the Django request context instead of the ContextVars? I don't have a strong feeling either way but wasn't sure if there was a best practice method Django recommended for variables that live for the length of a request and need to be passed around to different functions? The only issues I could foresee is if Django actually uses multiple threads to respond to a request and doesn't copy the context over which would mean the embargo date wouldn't always be set correctly. I think this is really unlikely but just wanted to raise it as a concern related to what Django best practice is.
As an aside, I would have much preferred if this had been worked on in a series of commits rather than one fat one. It's not good for future maintainability/the git history, and it makes it much harder to review as I can't just look at the new commits created after my last review.
Hi Josh, Thanks for your comment. Yes, Django request context was considered and I appreciate that it is often the go-to approach in Django. However, Django request context needs to be passed around. This would break all of the code interfaces (signatures) and make the application more complicated. ContextVars allow the code to pull in request-scoped state, without passing state around. A few points about ContextVars:
Example code, below: import threading
import contextvars
embargo_date = contextvars.ContextVar("embargo_date")
def worker():
# ContextVar is available here (current execution context, current thread)
print(embargo_date.get())
def start_thread():
embargo_date.set("2026-05-14")
# Capture current context
ctx = contextvars.copy_context() # <================== use copy_context if you need the embargo_date
# Run worker inside copied context
thread = threading.Thread(target=lambda: ctx.run(worker)) # <===== run a thread within the context
thread.start()
thread.join()
``` |
Hi Josh, I have reworked the flow so that routes to "View Live" are now authenticated. Many Thanks! |
Hi Josh, Yes, I can see where you are coming from! In a large commit like this it must have been less than helpful having to do the second review, not having a baseline to review it against. |
37b3b50 to
8a7d589
Compare
Enable page previews to the configured frontend URL
- with set embargo date
- support for per-request disabling of caching
via Cache-Control headers from the client
5f25667 to
ec212db
Compare
PR Review #3 - Apply auth to View Live - move shared.py to appropriate locations - django UTC timezone consistency in virtual_clock.py - remove redundant embargo context helper - tidy up typing import as it is used only once (from typing as t) - default to None if page previews not enabled - fix keyword args issue in virtual_clock.py - remove positional arg task/CDD-1379-page-previews update idna to 3.15
ec212db to
2eb2ddb
Compare
|
There was a problem hiding this comment.
Looks good, thanks for making those changes JP 🎉
Couple of minor comments in line and a question:
- If I remove the
page_idor change it to a different valid value (e.g. the ID of another page) from either/previewor/nocacheURLs it still works fine - is this intended? If so do we need that query parameter?
Otherwise, this is good to go to QA as far as I'm concerned!
| raise PermissionDenied | ||
|
|
||
| embargo_time_value = request.GET.get("embargo_time") | ||
| route = request.GET.get("route", "nocache") |
There was a problem hiding this comment.
Any particular functional reason to default to nocache here instead of error if the route parameter isn't set?
|
|
||
| We register an admin redirect endpoint | ||
| (`/admin/preview-to-frontend/<pk>/`) that signs a short-lived | ||
| (`/admin/redirect-to-frontend/<pk>/`) that signs a short-lived |
There was a problem hiding this comment.
Extremely minor note, because we expose /admin as /cms-admin, the URL path starts cms-admin not admin.



Description
CDD-1379 - Page Previews
Depends Upon: Front End branch: task/CDD-1379-page-previews (please pull the latest)
Date: 2026-02-27
Ticket: https://ukhsa.atlassian.net/browse/CDD-1379?search_id=055fe61d-bee9-48d9-80bc-ffb0f1c26b76&referrer=quick-find
Authors: Jean-Pierre Fouche
Impact: Affects all pages - broad testing required
Testing: Comprehensive unit tests supplied. UAT needed.
Summary
Allow editors of headless composite pages to click a Preview button that immediately redirects them to the external frontend application, rather than opening the built-in Wagtail iframe preview. Preview URLs include a short-lived signed token so the frontend can safely fetch draft content from the CMS.
Additionally, allow users to select an embargo date in order to preview otherwise embargoed data.
Continued in ./changelog/2026-02-27/CDD-1379.page-previews.md
Fixes #CDD-1379
Type of change
Please select the options that are relevant.
Checklist: