feat: Brand alignment + registration tokens (Phase 3a) #22
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!22
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/19-brand-alignment-registration-tokens"
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
Replace blue/gold CSS with Westside red/black brand across all server-rendered pages (register, coach onboard, roster). Add
Parent.registration_tokencolumn for token-based pre-fill via/register?token=. Add admin endpointPOST /admin/generate-tokensfor bulk token creation.Changes
src/basketball_api/brand.py(new) — Shared brand design tokens and CSS extracted from~/west-side-basketball/css/style.css. All pages reference this module.src/basketball_api/models.py— Addedregistration_tokencolumn to Parent (String(100), unique, nullable)alembic/versions/004_add_parent_registration_token.py(new) — Migration for the new columnsrc/basketball_api/routes/register.py— Replaced blue/gold CSS with red/black brand. Added?token=query param support with pre-fill from Parent/Player. Token error page. Confirmation page now shows profile summary.src/basketball_api/routes/coach.py— Replaced inline_STYLEwith sharedCOACH_CSSfrom brand module. Fixed remaining inline#f5b731references.src/basketball_api/routes/roster.py— Replaced inline CSS with sharedROSTER_CSSfrom brand module.src/basketball_api/routes/admin.py(new) —POST /admin/generate-tokensendpoint, auth-protected (admin role), idempotent.src/basketball_api/main.py— Registered admin router at/admintests/test_register.py— Added tests for token pre-fill, invalid token, brand colors, email dedup QR flow, confirmation profile summarytests/test_admin.py(new) — Tests for generate-tokens: paid parents get tokens, idempotent, unpaid parents skippedTest Plan
pytest tests/ -v— 63 tests pass (all existing + 10 new)ruff checkandruff formatclean/register,/register?token=<valid>,/register?token=<invalid>,/coach/onboard?token=<valid>#0a1628,#f5b731) remnants in route HTML outputRelated
plan-2026-03-08-tryout-prep(Phase 3a)PR #22 Review
PR: feat: Brand alignment + registration tokens (Phase 3a)
Branch:
feature/19-brand-alignment-registration-tokensIssue: #19
Reviewer: QA Agent
BLOCKERS
1. Email dedup on QR walk-up flow (Scope Item #8) is incomplete -- no dedicated GET path for email lookup
Scope item #8 specifies: "QR code flow (no token): parent enters email, if paid, pre-fill + 'Already paid' -- no Stripe redirect." The current implementation handles this only at POST time (email dedup during form submission). There is no intermediate step where a walk-up parent can enter their email first and get a pre-filled form back before submitting. The flow currently works as: blank form -> submit -> if email matches paid parent, show "Payment Received" on confirmation page. This means the walk-up parent has to re-enter all their data (player name, height, etc.) even though it already exists from Stripe. The pre-fill benefit only happens through the token path.
This is a significant UX gap for the QR walk-up scenario. A parent who paid via Stripe and then walks up to the QR kiosk on tryout day will get a blank form and have to re-type everything. Consider adding either: (a) an email lookup step on GET
/registerthat accepts?email=and pre-fills if found, or (b) on POST, when the email matches a paid parent with existing data, redirect back to a pre-filled form rather than immediately submitting.2. Existing test
test_get_form_returns_htmlwill break -- assertion checks for "West Side Basketball" but title changed to "Westside Kings & Queens"The existing test at
tests/test_register.pyline 31 asserts"West Side Basketball" in response.text. The new register.py template uses "Westside Kings & Queens" as the header text. The PR branch version of test_register.py adds new tests but the WebFetch summaries indicate this existing assertion is still present. If the old assertion text was not updated, this test will fail. The new testtest_form_title_is_westsidelikely covers the correct title, but the old test needs its assertion updated too. (If this has already been updated in the branch and WebFetch just did not show it, this can be dismissed -- please verify.)NITS
1. Inline hex codes remain in
roster.pyfor status colors (#22c55e,#eab308,#737373)These are functional status indicator colors (green for paid, yellow for pending, gray for empty state), not brand colors, so they are acceptable. However, for consistency, consider defining
COLOR_SUCCESS = "#22c55e"andCOLOR_WARNING = "#eab308"inbrand.pyso all colors are centralized. Non-blocking.2.
coach.pyreassigns_STYLE = COACH_CSSrather than usingCOACH_CSSdirectlyThe intermediate variable
_STYLEis left as an alias forCOACH_CSS. This works but adds unnecessary indirection. The HTML templates in coach.py reference{_STYLE}-- consider renaming to{COACH_CSS}directly and removing the alias. Non-blocking.3.
admin.py--require_adminis a module-level dependency, not a route-level oneThe pattern
require_admin = require_role("admin")at module level works, but it creates a module-scoped dependency that test overrides must target by name (require_adminnotrequire_role). The test file correctly overrides it viaapp.dependency_overrides[require_admin], so this is consistent. Just noting the pattern is slightly different from howroster.pyusesrequire_role("admin", "coach")inline in the route decorator. Non-blocking.4.
_render_formsignature --is_paiddefault isFalsebut the walk-up path never explicitly passes itWhen
_render_formis called from the error-handling code paths (validation errors),is_paiddefaults toFalse. This is correct behavior but worth noting -- if a paid parent submits a form with validation errors, they lose their "Already Paid" badge on the re-rendered form. Minor UX detail. Non-blocking.5. Token generation uses
secrets.token_urlsafe(32)producing ~43-character tokensThis fits within the
String(100)column constraint, andtoken_urlsafe(32)provides 256 bits of entropy, which is more than sufficient. Good security practice.SCOPE COVERAGE
brand.pyuses#d42026,#0a0a0a,#141414, system font stack, spacing, border radius/register/coach/onboardCOACH_CSSfrom brand.py,#f5b731fully removedROSTER_CSSfrom brand.py, confirmation page uses brand tokensregistration_tokenon Parent004_add_parent_registration_token.py,String(100), unique, nullable/register?token=pre-fill_render_confirmationshows parent name, email, height, class, positionPOST /admin/generate-tokensTEST COVERAGE
SOP COMPLIANCE
feature/19-brand-alignment-registration-tokens-> Issue #19)plan-2026-03-08-tryout-prep).envfiles, or credentials committedfeat:convention)VERDICT: NOT APPROVED
Reason: Blocker #1 (email dedup walk-up pre-fill gap) represents a meaningful UX regression for the March 13 tryout day scenario. A parent who already paid via Stripe and walks up to the QR kiosk should not have to re-enter all their data. The token path handles this beautifully, but the walk-up path does not. This was explicitly called out in scope item #8 and user story US-1.
Blocker #2 (potential test breakage on the title assertion) should be verified -- if it has already been fixed on the branch, it can be dismissed.
Once Blocker #1 is addressed (even a minimal approach -- e.g., on POST, if email matches a paid parent with existing player data, show the pre-filled form instead of immediately creating/updating), this PR is ready to merge. The overall quality is high: brand.py is well-structured, the token system follows the established coach invite_token pattern, tests are solid, and the migration is clean.
QA Blocker Fixes Pushed
Blocker 1: Email-first gate (replaces old email dedup pre-fill)
GET /register(no token) now shows only an email input instead of the full form. New endpointPOST /register/check-emailhandles the lookup:The token path (
GET /register?token=abc123) is unchanged.Blocker 2: Title string fix
tests/conftest.pytenant name:"West Side Basketball"->"Westside Kings & Queens"scripts/seed.pytenant name: same fixFiles changed
src/basketball_api/routes/register.py-- added_render_email_gate(),_render_check_email_message(),POST /register/check-emailendpoint; updatedGET /registerno-token pathtests/test_register.py-- replacedTestRegistrationFormGetandTestEmailDedupQRFlowwithTestEmailGateandTestCheckEmail; addedtest_token_form_has_all_fieldstests/conftest.py-- fixed tenant namescripts/seed.py-- fixed tenant namePR #22 Review (Re-review)
BLOCKERS
None. Both previous blockers have been resolved:
Email-first gate -- FIXED.
GET /registerwith no token now renders_render_email_gate()which shows only an email input field. The testtest_get_register_no_token_shows_email_gateexplicitly asserts thatplayer_name,parent_name, andgraduating_classfields are NOT present in the response.POST /register/check-emailcorrectly routes paid parents to a "Check Your Email" message and unknown/unpaid emails to the full blank form with email pre-filled.Test assertion -- FIXED. The old
"West Side Basketball"assertion has been removed entirely. Tests now assert"Westside Kings"and"Player Registration"which match the actual page title"Westside Kings & Queens -- Player Registration".NITS
services/email.pystill uses old blue/gold colors (#0a1628,#f5b731) in the HTML email template at lines 53-61. The server-rendered pages are all correctly branded, but the Stripe confirmation email still has the old scheme. This is likely out of scope for Phase 3a (Phase 3b covers the email blast), but worth tracking so it does not get forgotten.Hardcoded Stripe payment link in
register.pyline 40 (STRIPE_TRYOUT_LINK). This works for the current tryout season but will need updating for future seasons. Not a blocker -- just a maintenance note.brand.pyline 3 references~/west-side-basketball/css/style.cssas the source of design tokens. The path is fine as a documentation reference, just noting the old repo name uses a hyphenated form while the brand name has been unified to "Westside".SCOPE COMPLIANCE
All 8 Phase 3a scope items verified:
#d42026,#0a0a0a,#141414)src/basketball_api/brand.py/register,/coach/onboard, roster, confirmationBRAND_CSS/COACH_CSS/ROSTER_CSSconstants; zero blue/gold in rendered pagesregistration_tokenon ParentString(100),unique=True,nullable=TrueGET /register?token=Xpre-fills form with Stripe dataGET /register(no token) shows email-first gatePOST /register/check-emailroutes correctly_render_confirmation()with payment badge and summary rowsPOST /admin/generate-tokensgenerates tokens for paid familiesrequire_role("admin"), idempotent (skips existing tokens)TEST COVERAGE
Comprehensive test coverage across 4 test files with 22+ test cases:
test_register.py:TestEmailGate(4),TestCheckEmail(4),TestRegistrationTokenPreFill(4),TestRegistrationFormPost(10+)test_admin.py:TestGenerateTokens(4) -- covers paid, idempotent, unpaid, mixed scenariostest_admin.pyis clean (mockUser+ dependency override forrequire_admin)SOP COMPLIANCE
feature/19-brand-alignment-registration-tokensreferences issue #19).envfiles, or credentials committed (verified: nosk_live,sk_test,whsec_patterns;.gitignorecovers.env)brand.py,admin.py, migration004,test_register.py(rewritten),test_admin.py,scripts/seed.pyregister.py,models.py,main.py,coach.py,roster.py,conftest.pySECURITY NOTES
require_role("admin")viapal_e_authsecrets.token_urlsafe(32)-- cryptographically securehtml.escape()consistently for XSS preventionescape(token, quote=True)-- correctVERDICT: APPROVED