View a policy version's content, and make policy text readable #128

Merged
thatguygriff merged 1 commits from feature/policy-version-viewer-and-readable-text into main 2026-07-28 20:11:07 +00:00
Owner

Closes #126 and #127 — both are policy-admin work that turned out to share a render path, so they ship together.

Viewing a version (#126)

Every row in the versions table gets a View button. The viewer shows that version's number, status, and body — published, archived, or draft alike — rendered exactly as students see it, so it doubles as a preview. The body is editable from there, and saving does one of two things:

  • draft → updated in place, no new version number
  • published or archived → saved as a new draft version, original untouched, viewer follows to the new draft

That asymmetry is the point: acceptances are recorded against policy_version_id, so text a student agreed to must never be rewritten, while a draft nobody has seen has nothing worth preserving. A version id belonging to another policy is neither viewable nor editable through the page.

PATCH /policies/{id}/versions/{vid} is unchanged — it already restricted in-place edits to drafts.

Readable policy text (#127)

A studio reported the acceptance box rendering the whole policy on one tiny line, words overlapping, with a horizontal scrollbar. Two causes:

  1. No markup meant no paragraphs. Bodies are typed into a bare textarea, so most are plain prose; the text was emitted verbatim and every blank line collapsed. PolicyVersion::bodyHtml() is now the single render path — wp_kses_post() then wpautop(), the treatment WordPress gives post content — feeding GET /policies, the signup form, and the admin viewer. Bodies written with markup pass through unchanged. Applied at render, not at save, so the editor still shows what was typed.
  2. .us-policy-body had no CSS at all and inherited whatever the theme did with an unstyled block in a form. It is now a bounded reading box: vertical scroll, overflow-x: hidden, overflow-wrap: break-word so a pasted URL cannot force the page sideways, explicit line-height, and a height bound so a long policy cannot push the accept checkbox off-screen.

Found while fixing it: RegistrationPage never enqueued frontend.css, unlike BookingPage and GroupClassPage — so the signup gate would not have received the new rules at all. Fixed here.

Tests

  • PolicyControllerTest — new file: the viewer renders a body and its edit form, the list links to it, a version from another policy is neither shown nor editable, editing a draft calls updateBody (and never addDraftVersion), editing a published or archived version does the reverse.
  • PolicyEndpointTest::testIndexReturnsPlainTextBodiesAsParagraphs — a two-paragraph plain-text body comes back as <p>…</p><p>…</p>. index() had no coverage before.
  • PolicyValueObjectsTest — an empty body renders as nothing, not <p></p>.
  • stubAutop() added to tests/Unit/TestCase.php, since three suites now need it.

composer test (655 tests, 1861 assertions), composer lint, and composer cs all pass.

Notes

No schema change, so no version bump. main already carries an untagged 1.2.3, so the CSS is cache-busted by that release — nothing extra needed for the stylesheet to reach returning students.

One follow-up worth confirming with the reporter: a policy pasted from Word, where lines are joined by &nbsp; rather than blank lines, is the one shape wpautop() still will not break into paragraphs.

🤖 Generated with Claude Code

Closes #126 and #127 — both are policy-admin work that turned out to share a render path, so they ship together. ## Viewing a version (#126) Every row in the versions table gets a **View** button. The viewer shows that version's number, status, and body — published, archived, or draft alike — rendered exactly as students see it, so it doubles as a preview. The body is editable from there, and saving does one of two things: - **draft** → updated in place, no new version number - **published or archived** → saved as a **new draft version**, original untouched, viewer follows to the new draft That asymmetry is the point: acceptances are recorded against `policy_version_id`, so text a student agreed to must never be rewritten, while a draft nobody has seen has nothing worth preserving. A version id belonging to another policy is neither viewable nor editable through the page. `PATCH /policies/{id}/versions/{vid}` is unchanged — it already restricted in-place edits to drafts. ## Readable policy text (#127) A studio reported the acceptance box rendering the whole policy on one tiny line, words overlapping, with a horizontal scrollbar. Two causes: 1. **No markup meant no paragraphs.** Bodies are typed into a bare textarea, so most are plain prose; the text was emitted verbatim and every blank line collapsed. `PolicyVersion::bodyHtml()` is now the single render path — `wp_kses_post()` then `wpautop()`, the treatment WordPress gives post content — feeding `GET /policies`, the signup form, and the admin viewer. Bodies written with markup pass through unchanged. Applied at render, not at save, so the editor still shows what was typed. 2. **`.us-policy-body` had no CSS at all** and inherited whatever the theme did with an unstyled block in a form. It is now a bounded reading box: vertical scroll, `overflow-x: hidden`, `overflow-wrap: break-word` so a pasted URL cannot force the page sideways, explicit line-height, and a height bound so a long policy cannot push the accept checkbox off-screen. Found while fixing it: `RegistrationPage` never enqueued `frontend.css`, unlike `BookingPage` and `GroupClassPage` — so the signup gate would not have received the new rules at all. Fixed here. ## Tests - `PolicyControllerTest` — new file: the viewer renders a body and its edit form, the list links to it, a version from another policy is neither shown nor editable, editing a draft calls `updateBody` (and never `addDraftVersion`), editing a published or archived version does the reverse. - `PolicyEndpointTest::testIndexReturnsPlainTextBodiesAsParagraphs` — a two-paragraph plain-text body comes back as `<p>…</p><p>…</p>`. `index()` had no coverage before. - `PolicyValueObjectsTest` — an empty body renders as nothing, not `<p></p>`. - `stubAutop()` added to `tests/Unit/TestCase.php`, since three suites now need it. `composer test` (655 tests, 1861 assertions), `composer lint`, and `composer cs` all pass. ## Notes No schema change, so no version bump. `main` already carries an untagged 1.2.3, so the CSS is cache-busted by that release — nothing extra needed for the stylesheet to reach returning students. One follow-up worth confirming with the reporter: a policy pasted from Word, where lines are joined by `&nbsp;` rather than blank lines, is the one shape `wpautop()` still will not break into paragraphs. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
thatguygriff added 1 commit 2026-07-28 20:06:12 +00:00
View a policy version's content, and make policy text readable
CI / Tests (PHP 8.1) (pull_request) Successful in 50s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m12s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 2m50s
CI / PHPStan (pull_request) Successful in 3m4s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Build Plugin Zip (pull_request) Skipped
3a954bac57
The Policies admin page listed versions but never showed what any of them
said, so revising a policy meant retyping it blind into an empty draft box.
Each version row now has a View action that renders that version's text on
the page, editable in place. A draft is saved back to itself; editing a
published or archived version branches a new draft and leaves the original
alone, because acceptances are recorded against policy_version_id and text a
student agreed to must stay exactly as they saw it.

That viewer also exposed why a studio reported the acceptance box as
unreadable — one squashed line, overlapping words, a horizontal scrollbar.
Bodies are typed into a bare textarea, so most carry no markup, and the raw
text was emitted with its blank lines intact but nothing to turn them into
paragraphs. PolicyVersion::bodyHtml() now renders every body the way
WordPress renders post content (kses, then wpautop) and feeds all three
consumers: the booking/enrolment JSON, the signup form, and the new viewer.
Bodies written with markup are unaffected.

The other half was that .us-policy-body had no CSS whatsoever and inherited
whatever the theme did with an unstyled block in a form. It is now a bounded
reading box that scrolls vertically and breaks long tokens, so a pasted URL
cannot force the page sideways and a long policy cannot push the accept
checkbox out of view. RegistrationPage was also never enqueueing the plugin
stylesheet, which is why the signup gate looked worst of all.

Closes #126
Closes #127

Co-Authored-By: Claude Opus 5 <[email protected]>
thatguygriff merged commit fabbd35fa7 into main 2026-07-28 20:11:07 +00:00
thatguygriff deleted branch feature/policy-version-viewer-and-readable-text 2026-07-28 20:11:07 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Unsupervised/unsupervised-scheduler#128