feat: admin email -- profile reminders + roster export #102
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!102
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "101-admin-email-profile-completion-reminders"
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
roster_exportand make EmailLog FK columns nullable for admin-targeted emailsChanges
src/basketball_api/models.py: Addroster_exporttoEmailTypeenum; makeEmailLog.parent_idandEmailLog.player_idnullablesrc/basketball_api/routes/admin.py: AddGET /admin/players/incomplete,POST /admin/email/profile-reminder,POST /admin/email/roster-exportwith shared query helperssrc/basketball_api/services/email.py: Addsend_profile_reminder_email(),send_roster_export_email(), and_brand_wrapper()shared HTML helperalembic/versions/011_add_roster_export_email_type.py: Migration for enum value and nullable column changestests/test_admin_email.py: 21 tests for all new endpoints and service functionsTest Plan
pytest tests/ -k "incomplete or reminder or roster_export"-- 16 selected tests passruff checkandruff formatcleanReview Checklist
Related
PR #102 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy / Alembic / Pydantic -- applying Python/FastAPI domain checklist.
What this PR does: Adds three new admin endpoints (
GET /admin/players/incomplete,POST /admin/email/profile-reminder,POST /admin/email/roster-export), extends theEmailTypeenum withroster_export, makesEmailLog.parent_idandEmailLog.player_idnullable, adds a shared_brand_wrapper()HTML helper, and includes 21 new tests. Alembic migration 011 handles the schema changes.Positive observations:
_query_incomplete_players,_get_missing_fields) are extracted and reused between the incomplete-players endpoint and the reminder endpoint. Good DRY._brand_wrapper()is a good refactoring step -- shared HTML shell for new email types. The existing_build_confirmation_html()still has its own inline brand colors, but consolidating that is discovered scope, not this PR's job.html.escape()-d before insertion into HTML email bodies. Bothtenant_name,parent.name,player.name, and all roster fields go throughescape().require_admindependency. Auth denial tests exist for all three.IF NOT EXISTSfor the enum addition -- idempotent. Downgrade has appropriate comments about PostgreSQL enum limitations.EmailLogfor roster export correctly leavesparent_idandplayer_idasNone-- tested explicitly.Findings:
_query_incomplete_playersasymmetry withphoto_url-- The SQL filter checksPlayer.photo_url.is_(None)but does NOT checkPlayer.photo_url == "", while the other three fields (current_school,height,position) check for bothNoneand empty string. Meanwhile,_get_missing_fieldsusesnot player.photo_urlwhich catches bothNoneand"". If a player hasphoto_url=""in the database, they would NOT be returned by the query but_get_missing_fieldswould flagphoto_urlas missing. This is a minor inconsistency -- in practicephoto_urlis likely always either a URL orNone, never an empty string. But the pattern should be consistent. (NIT -- not a blocker since the query is conservative, not permissive.)PlayerData = dicttype alias is untyped (email.pyline 11) -- This isdictwith no key/value type hints. ATypedDictwould provide actual type safety and document the expected shape of the roster data dictionaries. Currently the roster export route builds raw dicts and the email service accesses them with.get()-- correct but fragile. (NIT)Roster export endpoint has no error handling --
admin_send_roster_exportcallssend_roster_export_email()without a try/except. If the Gmail API fails, the endpoint returns a raw 500. Compare with the profile-reminder endpoint which gracefully catches exceptions and returns them in anerrorslist. The asymmetry is notable -- one admin action fails gracefully, the other crashes. There is also no test for this failure path. (NIT -- admin-only endpoint, low blast radius, but worth noting.)Profile reminder uses
EmailType.reminderinstead of a distinct type -- The reminder email is logged asEmailType.reminder, which is the same type used for other reminder emails. This makes it impossible to distinguish profile-completion reminders from other reminder types in theemail_logtable. A dedicated enum value likeprofile_reminderwould improve auditability. (NIT)DRY: tenant lookup pattern duplicated --
db.query(Tenant).filter(Tenant.slug == DEFAULT_TENANT_SLUG).first()with the "Tenant not configured" HTTPException appears twice in this PR (lines 388 and 428) and also exists inregistration.py. This is a pre-existing pattern, but this PR adds two more copies. (NIT -- tracked in epilogue from PR #96 as pre-existing.)Hardcoded "Westside Kings & Queens" in email subject (
email.pyline 346:f"Complete {player.name}'s Profile -- Westside Kings & Queens") -- The tenant name is available viatenant.namebut the subject hardcodes the program name. The roster export subject similarly hardcodes "Westside Roster". Not a bug for a single-tenant deployment, but inconsistent with the_brand_wrapperwhich properly usestenant.name. (NIT)Migration downgrade sets FK values to 0 -- The downgrade sets
parent_id = 0andplayer_id = 0for NULL rows. There is almost certainly no parent or player withid = 0, so this would violate FK constraints in practice. The comment acknowledges this ("will fail if FK constraint is enforced, but this is best-effort rollback") which is honest. Best-effort is acceptable for a downgrade path that is unlikely to be exercised in production. (NIT)BLOCKERS
None. All new functionality has test coverage (21 tests). No unvalidated user input -- admin endpoints are auth-gated and user-supplied data in emails is HTML-escaped. No secrets or credentials in code. No DRY violations in auth/security paths.
NITS
Player.photo_url == ""to_query_incomplete_playersfor consistency with the other three field checks.PlayerData = dictwith aTypedDictfor type safety.admin_send_roster_exportfor parity with the profile-reminder error handling pattern (and add a test for it).profile_reminderenum value distinct fromreminderfor better email log auditability.tenant.namein email subjects instead of hardcoding "Westside Kings & Queens" / "Westside Roster".SOP COMPLIANCE
101-admin-email-profile-completion-remindersmatches issue #101)plan-wkqPROCESS OBSERVATIONS
VERDICT: APPROVED