feat: MJML template loader and jersey reminder endpoint #135
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/basketball-api!135
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "134-mjml-email-service"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Thin integration between basketball-api and westside-emails MJML compiled templates. Adds a template loader that reads compiled HTML and replaces
{{key}}placeholders via simple string substitution, plus a new admin endpoint for sending jersey reminder emails. Existing email functions are untouched -- migration happens incrementally.Changes
src/basketball_api/config.py-- addemail_templates_dirsetting (env varBASKETBALL_EMAIL_TEMPLATES_DIR, default/data/email-templates)src/basketball_api/services/email.py-- addload_email_template()function that reads compiled HTML from configurable directory and replaces{{key}}markers; addsend_jersey_reminder_email()that uses the template loader with{{name}}and{{jersey_url}}placeholderssrc/basketball_api/routes/admin.py-- addPOST /admin/email/jersey-reminderendpoint with optionaltest_emailquery param, matching the pattern of existing email endpoints (profile-reminder, tryout-announcement)tests/test_jersey_reminder.py-- 8 tests: 4 unit tests forload_email_template()(placeholder replacement, missing template error, unreplaced keys preserved, HTML special chars) and 4 integration tests for the endpoint (send to test email, send to all parents, no match returns 0, failure handling with error capture)Test Plan
ruff formatandruff checkpass with no issues{{name}}and{{jersey_url}}placeholdersReview Checklist
Related
plan-wkq-- Phase 11 discovered scopeAdd thin integration between basketball-api and westside-emails compiled HTML templates. Templates use literal {{key}} placeholders replaced via simple string substitution at send time -- no Jinja2, no template engine. - config.py: add email_templates_dir setting (BASKETBALL_EMAIL_TEMPLATES_DIR) - email.py: add load_email_template() for loading compiled HTML and replacing {{key}} placeholders, add send_jersey_reminder_email() using the template - admin.py: add POST /admin/email/jersey-reminder endpoint with optional test_email query param, matching existing email endpoint patterns - test_jersey_reminder.py: 8 tests covering template loader (placeholder replacement, missing template, unreplaced keys, special chars) and endpoint (send to test email, send to all, no match, failure handling) Existing email functions untouched -- migration happens incrementally. Refs #134 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>Review-fix loop
Finding 1 (fixed):
osandpathlib.Pathwere imported inside theload_email_template()function body instead of at module level. Moved to top-level imports in commit6415f38.Finding 2 (noted, acceptable):
send_jersey_reminder_emaillogs withEmailType.announcementsince there is nojersey_reminderenum value. Adding a new enum would require an Alembic migration (DB schema change), which is out of scope for this PR. Can be added in a follow-up if needed.All 464 tests pass.
ruff formatandruff checkclean.PR #135 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy / Pydantic -- applying Python/FastAPI domain checklist.
Pattern conformance: The new endpoint and email service function follow the established patterns closely. The
POST /admin/email/jersey-reminderendpoint mirrors the structure of/email/profile-reminder(lines 386-419 ofadmin.py) and/email/tryout-announcementwith the same{sent_count, errors}response model,require_admindependency, tenant lookup, parent query loop, and error capture. Well done.Template loader design:
load_email_template()is a clean, minimal function. Hardcoded template name at the only call site ("jersey-reminder") means no user-controlled path input today. The{{key}}string replacement is appropriate for the use case -- no need for Jinja2 overhead.Graceful fallback: When the MJML template is missing, the function falls back to
plain_body(setshtml_body = None). The plain text body is well-written with all the relevant info. This is good resilience for first deployment before the template volume is mounted.Type hints and docstrings: All new functions have proper type hints and PEP 257 docstrings. Parameters and return types are documented.
Test quality: 8 tests covering:
Tests use
tmp_pathfor template fixtures and mock Gmail client properly. Fixtures reuse the existingconftest.pypatterns (db,tenant).BLOCKERS
None. No blocker criteria are triggered:
test_emailquery param is used only as a SQLAlchemy filter value (Parent.email == test_email), which is parameterized -- no SQL injection risk. Thetemplate_nameinload_email_templateis hardcoded at the call site, not user-supplied.require_admindependency -- no duplication.NITS
EmailType reuse (
email.py:1020): Jersey reminder emails are logged asEmailType.announcement, making them indistinguishable from tryout announcement emails in theemail_logtable. Consider adding ajersey_remindervalue to theEmailTypeenum (with an Alembic migration) so email logs can be filtered by type. Not blocking since the existing announcement email also uses this type, but it will matter when you need to audit "which parents got the jersey email."Path traversal hardening (
email.py:935-936): Whiletemplate_nameis currently hardcoded, if a future caller ever passes user input, thePath(templates_dir) / f"{template_name}.html"construction would allow path traversal (e.g.,../../etc/passwd). A one-line guard likeif "/" in template_name or ".." in template_name: raise ValueError(...)would make this defense-in-depth. Low priority since the current call site is safe.datadict type hint (email.py:923):data: dictcould bedata: dict[str, str]for stricter typing, since all values go throughstr(value)anyway. Minor.htmlimport unused in new code:from html import escapeis imported at the top ofemail.pybut the new template loader does NOT escapeparent.namebefore inserting into HTML. This is consistent with the existing pattern (e.g.,send_tryout_announcement_emailalso insertsparent.nameunescaped into plain text bodies), but worth noting as pre-existing tech debt. If a parent registers with a name containing HTML entities, the MJML template would render it raw. Low risk since names come from your own registration form, but a future hardening task.SOP COMPLIANCE
134-mjml-email-servicefor issue #134)plan-wkq-- Phase 11 discovered scope)PROCESS OBSERVATIONS
westside-emails dist/directory atEMAIL_TEMPLATES_DIR.westside-emailsPR #2 for the compiled template. The test plan correctly marks the integration test as pending ([ ] Deploy with westside-emails dist/ mounted). Good awareness.VERDICT: APPROVED