R1.3 Template catalog: versions, preview, test send, typed context - #16
Merged
Merged
Conversation
Transactional templates gain an editable draft plus immutable published versions (publish endpoint, version list, per-send template_version with latest-published default), a markdown template format with YAML frontmatter rendered through the shared email shell, a render-preview endpoint with preview parity and missing-context reporting, a test send gated by TRANSACTIONAL_TEST_SEND_ALLOWLIST, and an import_templates command for AISL-format markdown directories. Closes #12
This was referenced Sep 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #12
Goal
Give transactional templates an editable draft plus immutable published versions, a markdown template format with typed required context, a render-preview endpoint, a controlled test send to an allowlisted staff address, and a management command that imports AI Shipping Labs-format markdown templates.
What changed
EmailTemplategainsmarkdown_bodyandcategory; new write-onceEmailTemplateVersion(email_template_versions, immutable via save/delete guards);TransactionalMessagerecordstemplate_version. Migration0026included;makemigrations --check --dry-runis clean.mailing/services/transactional.py): sends accept optionaltemplate_version(default: latest published version, else the draft for never-published templates); rendering goes through a render source so markdown drafts and published snapshots render correctly in single sends, dry runs, and both recipient-list send paths; required context validates against the resolved source (draft or version) with onecontext.<name>: requirederror per missing key, before any contact, message, or queue work; the queue payload carriestemplate_versionwhen set.mailing/views.py,mailing/urls.py,mailing/services/api.py):PUTtemplate upsert now acceptsmarkdown_bodyandcategory(andGETreports them pluslatest_version); new endpointsPOST .../publish(201, version N),GET .../versions,POST .../preview,POST .../test-sendfollowing the existing auth/error shapes.missing_context; rendering is identical to adry_runsend for the same version and context.TRANSACTIONAL_TEST_SEND_ALLOWLIST(empty disables the endpoint with 403; non-allowlisted addresses get 403); messages carry"test_send": truemetadata.mailing/services/transactional_markdown.py,mailing/templates/mailing/email_base.html): ports the AISL email pipeline (Django template substitution first, markdown with the donor extension set, shared HTML shell with header/footer, external links gettarget="_blank" rel="noopener", inline dash-run lists normalized).import_templates --dir <path> --client <slug>: imports AISL-format markdown files as drafts (keyed by file stem), rerunnable, per-file errors reported and fail the run.TRANSACTIONAL_TEST_SEND_ALLOWLIST,RELAY_EMAIL_BRAND_NAME,RELAY_EMAIL_SITE_BASE_URL. Deps:markdown>=3.6,python-frontmatter>=1.1.0(plus transitivepyyaml); theuv.lockdiff touches only these.docs/api.mddocuments versions, preview, test send,template_versionon send, and the frontmatter format;docs/data-model.mdcoversemail_template_versionsand the new fields.Verification
uv run pytest(full suite):(includes 23 new tests in
mailing/tests/test_transactional_template_versions.pyandmailing/tests/test_import_templates_command.py: publish immutability, version defaulting and explicit-version sends, preview parity withdry_run, required-context per-key failures before any work, unknown/invalid version handling, versions listing, test-send allowlist enforcement, markdown shell rendering, and import command behavior)uv run ruff check .:uv run python manage.py makemigrations --check --dry-run:Import of the AISL template directory against a disposable local SQLite database:
Preview of
event_registrationwith the AISL preview context fromemail_app/services/preview_contexts.py(addresses sanitized for this log):Not run here, needs:
SQS_TRANSACTIONAL_EMAIL_QUEUE_URLconfigured).Notes
missing_contextin the preview response is the operator signal.catalog_context) still previews rawhtml_body/text_body, so markdown drafts show an empty preview there; the API preview endpoint covers markdown. Left out to keep this PR scoped to the issue; can follow up separately.uv0.10.11 rewritesuv.lockon plainuv sync/uv run(drops the[options]exclude-newer pins). The committed lock keeps the pins exactly as onmainand only adds the three packages; the same rewrite would happen onmaintoday, so nothing in this PR changes that behavior.