DataTalksClub / DataTalksClub/datamailer
Template catalog: versions, preview, test send, typed context
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 3
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Template catalog: versions, preview, test send, typed context
Status: pending
Tags: P1, backend, api, transactional, email, docs
Depends on: None
Blocks: — (downstream unification-plan adoption work depends on this; tracked in community-base, not in this repo)
Source: unification plan issue R1.3 (community-base docs/plan/phase-1.md). Relay is the unification plan's email and jobs service; this issue sits on the plan's external critical path and may proceed independently.
Scope
- Add immutable published versions to the transactional template catalog. Each client template keeps one editable draft;
PUT /api/transactional/templates/{key}keeps writing the draft; publishing snapshots the draft into a new immutable version. - Template format: markdown body with YAML frontmatter (
subjectrequired;required_contextandcategoryoptional). Rendering ports the AISL pipeline: render the context into the source with the Django template engine, convert markdown to sanitized HTML, and wrap with the shared HTML wrapper and footer (port of_render_template_with_footerand its wrapper). Plain text derives from the same source. - Direct-HTML drafts stay supported:
html_body/text_bodyinPUTremain a first-class content form for templates provisioned through the API (CMP provisioning uses it). Markdown and direct HTML are two content forms in one catalog; the HTML form is not a legacy path. - Sends resolve
template_keyplus optionaltemplate_version(default: latest published).required_contextis validated against the resolved version on every transactional send path; missing keys fail the send with a clear error before any contact, message, or queue work. - New client-Bearer endpoints under the authenticated template scope:
POST /api/transactional/templates/{key}/publish,GET /api/transactional/templates/{key}/versions,POST /api/transactional/templates/{key}/preview(returns subject, HTML, text for the draft or a version), andPOST /api/transactional/templates/{key}/test-send(sends a version to an allowlisted staff address). - Management command
python manage.py import_templates --dir <path> --client <slug>creates drafts from a directory of AISL-format markdown files; the plan uses it in phase 6 and DTC uses it for its templates. - Datamailer is pre-production: no compatibility shims, aliases, or duplicated endpoints. The AISL markdown import is an explicitly scoped client-integration requirement, built as a first-class Datamailer command and format.
Codebase notes for the implementer
EmailTemplate(mailing/models.py) already hasrequired_context(both plain strings and{name, description}items are accepted bynormalize_required_context),example_context,default_sender_id,is_transactional,is_active. It has no version, category, or markdown-source fields; add what the design needs.get_transactional_templateinmailing/services/transactional.pycurrently resolves the mutable template row for all three send paths (single send, recipient-list send, transient-list send). After this change it resolves published versions; a template with no published version must fail the send instead of silently using the draft.validate_transactional_send_payloadgainstemplate_version.validate_template_context(mailing/services/transactional_catalog.py) already fails sends on missingrequired_contextwithcontext.<name>: required(400, before contact creation). Keep that error contract; make it version-aware.TransactionalMessagealready snapshots rendered subject/html/text at enqueue time and the sender worker sends the stored bodies, so the SQS queue payload and worker stay unchanged; persist the resolvedtemplate_versionon the message for auditability.- The frontmatter
categoryis template metadata. It is not the send-levelcategory_tag(subscription preference category) that sends already accept; do not conflate them. - No markdown/YAML/frontmatter libraries are in
pyproject.tomlyet; add what the renderer and importer need. - The route
api/transactional/templates/<slug:template_key>accepts AISL-style keys containing underscores, so imported file stems work as keys unchanged. - New endpoints must be registered in the in-app docs (
build_openapi_specand the endpoint map inmailing/services/api_docs.py);test_api_docs_endpoint_reference_matches_openapi_pathsenforces that the endpoint reference matches OpenAPI paths. - There is no version backfill for pre-existing templates. Sandbox environments re-run
seed_demo_dataandscripts/upsert_cmp_templates.py; update both to publish after upsert so existing flows keep sending. - The staff operator UI already previews drafts at
/templates/and/templates/<id>/(render_previewintransactional_catalog.py); keep it working and reuse service functions where sensible. It is a different surface from the new API preview.
Donor references (read first)
docs/api.md"Transactional Email API" and theemail_templatestable indocs/data-model.md~/git/dtc-website/_docs/specs/05-events-registration-email.md, section "Relay-owned email templates"~/git/ai-shipping-labs/email_app/email_templates/(the markdown format that must import cleanly),email_app/services/email_service.py_render_template_with_footer, andemail_app/services/preview_contexts.py
Verification (from the plan issue)
- Import
~/git/ai-shipping-labs/email_app/email_templates/into the sandbox with the command -> 50 drafts created, zero errors. - Preview of
event_registrationwith the AISL preview context (email_app/services/preview_contexts.py) renders without missing keys.
Acceptance Criteria
- Publish:
POST /api/transactional/templates/{key}/publishsnapshots the current draft as immutable version N (1-based per client and key) storing subject, the canonical content source (markdown body and/or html/text),required_context,category, and the publish timestamp. Publishing a draft whose content matches the latest published version returns that version without creating a new row. - Immutability: no API, management command, or admin action can modify a published version;
PUTand every later republish touch only the draft. -
GET /api/transactional/templates/{key}/versionslists versions with version number, subject,category, published timestamp, and which is latest;GET /api/transactional/templates/{key}returns the draft pluslatest_published_version. - Send resolution:
POST /api/transactional/send, the recipient-list transactional send, and the transient-list transactional send accept optionaltemplate_version; omitted means latest published; an unknown version returns 404; a template with no published version returns 409 with ano_published_versionerror. A failed resolution creates no contact, no message row, and no queue payload. - Required context: every send path validates the resolved version's
required_contextand rejects with 400 naming each missingcontext.<name>before creating the contact or message. - Auditability:
TransactionalMessagestores the resolvedtemplate_version, and the send response and dry-run response include it next totemplate_key. - Preview:
POST /api/transactional/templates/{key}/previewaccepts{context, template_version?}(draft when no version is given) and returns{subject, html, text}; missing required context keys return a 400 listing them. Preview and send share one render code path: the same version and context produce identical subject/html/text through preview anddry_runsend. - Test send:
POST /api/transactional/templates/{key}/test-sendsends the given version (default: latest published) to an address matched against a deny-by-default staff-address allowlist; a non-allowlisted address is rejected with a clear error and nothing is enqueued; an allowed request goes through the normal enqueue path and marks the message as a test send in metadata. - Import:
python manage.py import_templates --dir <path> --client <slug>creates or updates one draft per*.mdfile (key = file stem), is idempotent on re-run, treatsrequired_contextandcategoryas optional frontmatter, and exits non-zero with a clear message on a malformed file. - Plan verification: importing
/home/alexey/git/ai-shipping-labs/email_app/email_templates/creates 50 drafts with zero errors, and previewingevent_registrationwith its AISL preview context renders without missing keys. -
docs/api.mddocuments versions, publish, preview, test send,template_versionon sends, and the frontmatter format. The in-app docs at/api-docs/stay in sync (OpenAPI paths, endpoint reference, runnable examples), anddocs/data-model.mdreflects the draft/version model andtransactional_messages.template_version. - Existing flows keep working after the resolution change:
seed_demo_dataandscripts/upsert_cmp_templates.pypublish after upsert, and the existing transactional API, dry-run, CMP template script, and LocalStack integration tests pass (updated where they now need a published version). - [HUMAN] One test-send on the sandbox to an allowlisted staff address arrives and renders correctly in a real mail client (subject, wrapped HTML with footer, readable plain text).
Test Notes
- Unit/service tests: publish and immutability; version resolution (latest default, explicit, unknown, none published); required-context validation; frontmatter parsing (with and without optional keys); import command against a fixture directory including a malformed file; preview/send render parity.
- API/view tests: PUT draft semantics and GET shape; publish, versions list, preview, test-send (allowed, denied, another client's template); send with and without
template_version; dry-run parity; api_docs/OpenAPI registration. - LocalStack/mocked AWS tests: SES payloads for a versioned send and a test-send asserted with
botocore.stub.Stubber; no test sends real email. - UI/screenshot checks: not applicable; no visible operator UI change. Tester should state screenshots are not applicable.
Blocked by: None.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
The implementation spans mailing/models.py, mailing/services/transactional.py, mailing/services/transactional_catalog.py, the transactional API routes, docs/api.md, and the seed/import paths; start by reviewing the existing LocalStack and API tests. Finish the remaining sandbox test-send to an allowlisted staff address and confirm the subject, wrapped HTML with footer, and plain text in a real mail client.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- django, python
- Domain
- api, backend, documentation, testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100